From 9e95278ffd29074a6b571947fdadbb3a83e8cf2f Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 15 Aug 2024 11:03:54 +1000 Subject: [PATCH 01/94] [feat] Create new Select component --- .../select/SelectIntroduction/system/index.js | 17 + .../SelectIntroduction/system/index.tsx | 17 + .../system/index.tsx.preview | 10 + docs/data/base/components/select/select.md | 17 + docs/data/base/pages.ts | 94 ++++ docs/data/base/pagesApi.js | 403 ++++++++++++++++++ .../base-ui/react-select/[docsTab]/index.js | 84 ++++ .../api-docs/select-item/select-item.json | 15 + .../api-docs/select-popup/select-popup.json | 11 + .../select-positioner/select-positioner.json | 43 ++ .../api-docs/select-root/select-root.json | 22 + .../select-trigger/select-trigger.json | 15 + .../use-select-item/use-select-item.json | 1 + .../use-select-positioner.json | 1 + .../use-select-root/use-select-root.json | 1 + .../use-select-trigger.json | 1 + docs/translations/translations.json | 244 +++++++++++ .../mui-base/src/Select/Item/SelectItem.tsx | 174 ++++++++ .../mui-base/src/Select/Item/useSelectItem.ts | 85 ++++ .../mui-base/src/Select/Popup/SelectPopup.tsx | 97 +++++ .../Select/Positioner/SelectPositioner.tsx | 274 ++++++++++++ .../Positioner/SelectPositionerContext.ts | 31 ++ .../Select/Positioner/useSelectPositioner.tsx | 203 +++++++++ .../mui-base/src/Select/Root/SelectRoot.tsx | 151 +++++++ .../src/Select/Root/SelectRootContext.ts | 23 + .../src/Select/Root/useSelectRoot.tsx | 222 ++++++++++ .../src/Select/Trigger/SelectTrigger.tsx | 105 +++++ .../src/Select/Trigger/useSelectTrigger.ts | 138 ++++++ packages/mui-base/src/Select/index.barrel.ts | 5 + packages/mui-base/src/Select/index.ts | 5 + .../src/Select/utils/commonStyleHooks.ts | 5 + 31 files changed, 2514 insertions(+) create mode 100644 docs/data/base/components/select/SelectIntroduction/system/index.js create mode 100644 docs/data/base/components/select/SelectIntroduction/system/index.tsx create mode 100644 docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview create mode 100644 docs/data/base/components/select/select.md create mode 100644 docs/data/base/pages.ts create mode 100644 docs/data/base/pagesApi.js create mode 100644 docs/pages/base-ui/react-select/[docsTab]/index.js create mode 100644 docs/translations/api-docs/select-item/select-item.json create mode 100644 docs/translations/api-docs/select-popup/select-popup.json create mode 100644 docs/translations/api-docs/select-positioner/select-positioner.json create mode 100644 docs/translations/api-docs/select-root/select-root.json create mode 100644 docs/translations/api-docs/select-trigger/select-trigger.json create mode 100644 docs/translations/api-docs/use-select-item/use-select-item.json create mode 100644 docs/translations/api-docs/use-select-positioner/use-select-positioner.json create mode 100644 docs/translations/api-docs/use-select-root/use-select-root.json create mode 100644 docs/translations/api-docs/use-select-trigger/use-select-trigger.json create mode 100644 docs/translations/translations.json create mode 100644 packages/mui-base/src/Select/Item/SelectItem.tsx create mode 100644 packages/mui-base/src/Select/Item/useSelectItem.ts create mode 100644 packages/mui-base/src/Select/Popup/SelectPopup.tsx create mode 100644 packages/mui-base/src/Select/Positioner/SelectPositioner.tsx create mode 100644 packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts create mode 100644 packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx create mode 100644 packages/mui-base/src/Select/Root/SelectRoot.tsx create mode 100644 packages/mui-base/src/Select/Root/SelectRootContext.ts create mode 100644 packages/mui-base/src/Select/Root/useSelectRoot.tsx create mode 100644 packages/mui-base/src/Select/Trigger/SelectTrigger.tsx create mode 100644 packages/mui-base/src/Select/Trigger/useSelectTrigger.ts create mode 100644 packages/mui-base/src/Select/index.barrel.ts create mode 100644 packages/mui-base/src/Select/index.ts create mode 100644 packages/mui-base/src/Select/utils/commonStyleHooks.ts diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js new file mode 100644 index 000000000..762ad7aca --- /dev/null +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -0,0 +1,17 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; + +export default function UnstyledSelectIntroduction() { + return ( + + Trigger + + + Item 1 + Item 2 + Item 3 + + + + ); +} diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx new file mode 100644 index 000000000..762ad7aca --- /dev/null +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; + +export default function UnstyledSelectIntroduction() { + return ( + + Trigger + + + Item 1 + Item 2 + Item 3 + + + + ); +} diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview new file mode 100644 index 000000000..db7e28b51 --- /dev/null +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview @@ -0,0 +1,10 @@ + + Trigger + + + Item 1 + Item 2 + Item 3 + + + \ No newline at end of file diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md new file mode 100644 index 000000000..a8a384eeb --- /dev/null +++ b/docs/data/base/components/select/select.md @@ -0,0 +1,17 @@ +--- +productId: base-ui +title: React Select components and hook +components: SelectRoot, SelectTrigger, SelectPositioner, SelectPopup, SelectItem +githubLabel: 'component: select' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ +--- + +# Select + +

Select provides users with a floating element containing a list of options to choose from.

+ +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + +{{"component": "modules/components/ComponentPageTabs.js"}} + +{{"demo": "SelectIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts new file mode 100644 index 000000000..dff263ebb --- /dev/null +++ b/docs/data/base/pages.ts @@ -0,0 +1,94 @@ +import type { MuiPage } from 'docs/src/MuiPage'; + +const pages: readonly MuiPage[] = [ + { + pathname: '/base-ui/getting-started-group', + title: 'Getting started', + children: [ + { pathname: '/base-ui/getting-started', title: 'Overview' }, + { pathname: '/base-ui/getting-started/quickstart', title: 'Quickstart' }, + { pathname: '/base-ui/getting-started/usage', title: 'Usage' }, + { pathname: '/base-ui/getting-started/accessibility', title: 'Accessibility' }, + { pathname: '/base-ui/getting-started/support' }, + ], + }, + { + pathname: '/base-ui/react-', + title: 'Components', + children: [ + { pathname: '/base-ui/all-components', title: 'All components' }, + { + pathname: '/base-ui/components/inputs', + subheader: 'inputs', + children: [ + // { pathname: '/base-ui/react-autocomplete', title: 'Autocomplete' }, + { pathname: '/base-ui/react-checkbox', title: 'Checkbox' }, + { pathname: '/base-ui/react-number-field', title: 'Number Field' }, + // { pathname: '/base-ui/react-radio-group', title: 'Radio Group', planned: true }, + { pathname: '/base-ui/react-select', title: 'Select' }, + { pathname: '/base-ui/react-slider', title: 'Slider' }, + { pathname: '/base-ui/react-switch', title: 'Switch' }, + ], + }, + { + pathname: '/base-ui/components/data-display', + subheader: 'data-display', + children: [ + { pathname: '/base-ui/react-popover', title: 'Popover' }, + { pathname: '/base-ui/react-preview-card', title: 'Preview Card' }, + { pathname: '/base-ui/react-tooltip', title: 'Tooltip' }, + { pathname: '/base-ui/react-field', title: 'Field' }, + { pathname: '/base-ui/react-fieldset', title: 'Fieldset' }, + ], + }, + { + pathname: '/base-ui/components/feedback', + subheader: 'feedback', + children: [ + { pathname: '/base-ui/react-alert-dialog', title: 'Alert Dialog' }, + { pathname: '/base-ui/react-dialog', title: 'Dialog' }, + { pathname: '/base-ui/react-progress', title: 'Progress' }, + ], + }, + { + pathname: '/base-ui/components/navigation', + subheader: 'navigation', + children: [ + { pathname: '/base-ui/react-menu', title: 'Menu' }, + // { pathname: '/base-ui/react-table-pagination', title: 'Table Pagination' }, + { pathname: '/base-ui/react-tabs', title: 'Tabs' }, + ], + }, + // { + // pathname: '/base-ui/components/utils', + // subheader: 'utils', + // children: [ + // { pathname: '/base-ui/react-click-away-listener', title: 'Click-Away Listener' }, + // { pathname: '/base-ui/react-focus-trap', title: 'Focus Trap' }, + // { pathname: '/base-ui/react-form-control', title: 'Form Control' }, + // { pathname: '/base-ui/react-no-ssr', title: 'No-SSR' }, + // { pathname: '/base-ui/react-popup', title: 'Popup', unstable: true }, + // { pathname: '/base-ui/react-portal', title: 'Portal' }, + // { pathname: '/base-ui/react-textarea-autosize', title: 'Textarea Autosize' }, + // ], + // }, + ], + }, + /* { + title: 'APIs', + pathname: '/base-ui/api', + children: pagesApi, + }, */ + { + pathname: '/base-ui/guides', + title: 'How-to guides', + children: [ + { + pathname: '/base-ui/guides/next-js-app-router', + title: 'Next.js App Router', + }, + ], + }, +]; + +export default pages; diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js new file mode 100644 index 000000000..4423259c6 --- /dev/null +++ b/docs/data/base/pagesApi.js @@ -0,0 +1,403 @@ +module.exports = [ + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-backdrop', + title: 'AlertDialogBackdrop', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-close', + title: 'AlertDialogClose', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-description', + title: 'AlertDialogDescription', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-popup', + title: 'AlertDialogPopup', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-root', + title: 'AlertDialogRoot', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-title', + title: 'AlertDialogTitle', + }, + { + pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-trigger', + title: 'AlertDialogTrigger', + }, + { + pathname: '/base-ui/react-checkbox/components-api/#checkbox-indicator', + title: 'CheckboxIndicator', + }, + { + pathname: '/base-ui/react-checkbox/components-api/#checkbox-root', + title: 'CheckboxRoot', + }, + { + pathname: + '/base-ui/react-click-away-listener/components-api/#click-away-listener', + title: 'ClickAwayListener', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-backdrop', + title: 'DialogBackdrop', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-close', + title: 'DialogClose', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-description', + title: 'DialogDescription', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-popup', + title: 'DialogPopup', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-root', + title: 'DialogRoot', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-title', + title: 'DialogTitle', + }, + { + pathname: '/base-ui/react-dialog/components-api/#dialog-trigger', + title: 'DialogTrigger', + }, + { + pathname: '/base-ui/react-field/components-api/#field-control', + title: 'FieldControl', + }, + { + pathname: '/base-ui/react-field/components-api/#field-description', + title: 'FieldDescription', + }, + { + pathname: '/base-ui/react-field/components-api/#field-error', + title: 'FieldError', + }, + { + pathname: '/base-ui/react-field/components-api/#field-label', + title: 'FieldLabel', + }, + { + pathname: '/base-ui/react-field/components-api/#field-root', + title: 'FieldRoot', + }, + { + pathname: '/base-ui/react-field/components-api/#field-validity', + title: 'FieldValidity', + }, + { + pathname: '/base-ui/react-fieldset/components-api/#fieldset-legend', + title: 'FieldsetLegend', + }, + { + pathname: '/base-ui/react-fieldset/components-api/#fieldset-root', + title: 'FieldsetRoot', + }, + { + pathname: '/base-ui/react-focus-trap/components-api/#focus-trap', + title: 'FocusTrap', + }, + { + pathname: '/base-ui/react-form-control/components-api/#form-control', + title: 'FormControl', + }, + { pathname: '/base-ui/react-menu/components-api/#menu-arrow', title: 'MenuArrow' }, + { pathname: '/base-ui/react-menu/components-api/#menu-item', title: 'MenuItem' }, + { pathname: '/base-ui/react-menu/components-api/#menu-popup', title: 'MenuPopup' }, + { + pathname: '/base-ui/react-menu/components-api/#menu-positioner', + title: 'MenuPositioner', + }, + { pathname: '/base-ui/react-menu/components-api/#menu-root', title: 'MenuRoot' }, + { + pathname: '/base-ui/react-menu/components-api/#menu-trigger', + title: 'MenuTrigger', + }, + { pathname: '/base-ui/react-no-ssr/components-api/#no-ssr', title: 'NoSsr' }, + { + pathname: '/base-ui/react-number-field/components-api/#number-field-decrement', + title: 'NumberFieldDecrement', + }, + { + pathname: '/base-ui/react-number-field/components-api/#number-field-group', + title: 'NumberFieldGroup', + }, + { + pathname: '/base-ui/react-number-field/components-api/#number-field-increment', + title: 'NumberFieldIncrement', + }, + { + pathname: '/base-ui/react-number-field/components-api/#number-field-input', + title: 'NumberFieldInput', + }, + { + pathname: '/base-ui/react-number-field/components-api/#number-field-root', + title: 'NumberFieldRoot', + }, + { + pathname: '/base-ui/react-number-field/components-api/#number-field-scrub-area', + title: 'NumberFieldScrubArea', + }, + { + pathname: + '/base-ui/react-number-field/components-api/#number-field-scrub-area-cursor', + title: 'NumberFieldScrubAreaCursor', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-arrow', + title: 'PopoverArrow', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-backdrop', + title: 'PopoverBackdrop', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-close', + title: 'PopoverClose', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-description', + title: 'PopoverDescription', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-popup', + title: 'PopoverPopup', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-positioner', + title: 'PopoverPositioner', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-root', + title: 'PopoverRoot', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-title', + title: 'PopoverTitle', + }, + { + pathname: '/base-ui/react-popover/components-api/#popover-trigger', + title: 'PopoverTrigger', + }, + { pathname: '/base-ui/react-popup/components-api/#popup', title: 'Popup' }, + { pathname: '/base-ui/react-portal/components-api/#portal', title: 'Portal' }, + { + pathname: '/base-ui/react-preview-card/components-api/#preview-card-arrow', + title: 'PreviewCardArrow', + }, + { + pathname: '/base-ui/react-preview-card/components-api/#preview-card-backdrop', + title: 'PreviewCardBackdrop', + }, + { + pathname: '/base-ui/react-preview-card/components-api/#preview-card-popup', + title: 'PreviewCardPopup', + }, + { + pathname: '/base-ui/react-preview-card/components-api/#preview-card-positioner', + title: 'PreviewCardPositioner', + }, + { + pathname: '/base-ui/react-preview-card/components-api/#preview-card-root', + title: 'PreviewCardRoot', + }, + { + pathname: '/base-ui/react-preview-card/components-api/#preview-card-trigger', + title: 'PreviewCardTrigger', + }, + { + pathname: '/base-ui/react-progress/components-api/#progress-indicator', + title: 'ProgressIndicator', + }, + { + pathname: '/base-ui/react-progress/components-api/#progress-root', + title: 'ProgressRoot', + }, + { + pathname: '/base-ui/react-progress/components-api/#progress-track', + title: 'ProgressTrack', + }, + { + pathname: '/base-ui/react-select/components-api/#select-item', + title: 'SelectItem', + }, + { + pathname: '/base-ui/react-select/components-api/#select-popup', + title: 'SelectPopup', + }, + { + pathname: '/base-ui/react-select/components-api/#select-positioner', + title: 'SelectPositioner', + }, + { + pathname: '/base-ui/react-select/components-api/#select-root', + title: 'SelectRoot', + }, + { + pathname: '/base-ui/react-select/components-api/#select-trigger', + title: 'SelectTrigger', + }, + { + pathname: '/base-ui/react-slider/components-api/#slider-control', + title: 'SliderControl', + }, + { + pathname: '/base-ui/react-slider/components-api/#slider-indicator', + title: 'SliderIndicator', + }, + { + pathname: '/base-ui/react-slider/components-api/#slider-output', + title: 'SliderOutput', + }, + { + pathname: '/base-ui/react-slider/components-api/#slider-root', + title: 'SliderRoot', + }, + { + pathname: '/base-ui/react-slider/components-api/#slider-thumb', + title: 'SliderThumb', + }, + { + pathname: '/base-ui/react-slider/components-api/#slider-track', + title: 'SliderTrack', + }, + { + pathname: '/base-ui/react-snackbar/components-api/#snackbar', + title: 'Snackbar', + }, + { + pathname: '/base-ui/react-menu/components-api/#submenu-trigger', + title: 'SubmenuTrigger', + }, + { + pathname: '/base-ui/react-switch/components-api/#switch-root', + title: 'SwitchRoot', + }, + { + pathname: '/base-ui/react-switch/components-api/#switch-thumb', + title: 'SwitchThumb', + }, + { pathname: '/base-ui/react-tabs/components-api/#tab', title: 'Tab' }, + { + pathname: '/base-ui/react-tabs/components-api/#tab-indicator', + title: 'TabIndicator', + }, + { pathname: '/base-ui/react-tabs/components-api/#tab-panel', title: 'TabPanel' }, + { + pathname: '/base-ui/react-table-pagination/components-api/#table-pagination', + title: 'TablePagination', + }, + { pathname: '/base-ui/react-tabs/components-api/#tabs-list', title: 'TabsList' }, + { pathname: '/base-ui/react-tabs/components-api/#tabs-root', title: 'TabsRoot' }, + { + pathname: '/base-ui/react-textarea-autosize/components-api/#textarea-autosize', + title: 'TextareaAutosize', + }, + { + pathname: '/base-ui/react-tooltip/components-api/#tooltip-arrow', + title: 'TooltipArrow', + }, + { + pathname: '/base-ui/react-tooltip/components-api/#tooltip-popup', + title: 'TooltipPopup', + }, + { + pathname: '/base-ui/react-tooltip/components-api/#tooltip-positioner', + title: 'TooltipPositioner', + }, + { + pathname: '/base-ui/react-tooltip/components-api/#tooltip-provider', + title: 'TooltipProvider', + }, + { + pathname: '/base-ui/react-tooltip/components-api/#tooltip-root', + title: 'TooltipRoot', + }, + { + pathname: '/base-ui/react-tooltip/components-api/#tooltip-trigger', + title: 'TooltipTrigger', + }, + { + pathname: '/base-ui/react-autocomplete/hooks-api/#use-autocomplete', + title: 'useAutocomplete', + }, + { + pathname: '/base-ui/react-checkbox/hooks-api/#use-checkbox-root', + title: 'useCheckboxRoot', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-close', + title: 'useDialogClose', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-popup', + title: 'useDialogPopup', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-root', + title: 'useDialogRoot', + }, + { + pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-trigger', + title: 'useDialogTrigger', + }, + { + pathname: '/base-ui/react-form-control/hooks-api/#use-form-control-context', + title: 'useFormControlContext', + }, + { + pathname: '/base-ui/react-number-field/hooks-api/#use-number-field-root', + title: 'useNumberFieldRoot', + }, + { + pathname: '/base-ui/react-progress/hooks-api/#use-progress-indicator', + title: 'useProgressIndicator', + }, + { + pathname: '/base-ui/react-progress/hooks-api/#use-progress-root', + title: 'useProgressRoot', + }, + { + pathname: '/base-ui/react-slider/hooks-api/#use-slider-control', + title: 'useSliderControl', + }, + { + pathname: '/base-ui/react-slider/hooks-api/#use-slider-indicator', + title: 'useSliderIndicator', + }, + { + pathname: '/base-ui/react-slider/hooks-api/#use-slider-output', + title: 'useSliderOutput', + }, + { + pathname: '/base-ui/react-slider/hooks-api/#use-slider-root', + title: 'useSliderRoot', + }, + { + pathname: '/base-ui/react-slider/hooks-api/#use-slider-thumb', + title: 'useSliderThumb', + }, + { + pathname: '/base-ui/react-snackbar/hooks-api/#use-snackbar', + title: 'useSnackbar', + }, + { + pathname: '/base-ui/react-switch/hooks-api/#use-switch-root', + title: 'useSwitchRoot', + }, + { pathname: '/base-ui/react-tabs/hooks-api/#use-tab', title: 'useTab' }, + { + pathname: '/base-ui/react-tabs/hooks-api/#use-tab-indicator', + title: 'useTabIndicator', + }, + { pathname: '/base-ui/react-tabs/hooks-api/#use-tab-panel', title: 'useTabPanel' }, + { pathname: '/base-ui/react-tabs/hooks-api/#use-tabs-list', title: 'useTabsList' }, + { pathname: '/base-ui/react-tabs/hooks-api/#use-tabs-root', title: 'useTabsRoot' }, +]; diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js new file mode 100644 index 000000000..8fddcfb73 --- /dev/null +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -0,0 +1,84 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; +import AppFrame from 'docs/src/modules/components/AppFrame'; +import * as pageProps from 'docs-base/data/base/components/select/select.md?@mui/markdown'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import SelectItemApiJsonPageContent from '../../api/select-item.json'; +import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; +import SelectPositionerApiJsonPageContent from '../../api/select-positioner.json'; +import SelectRootApiJsonPageContent from '../../api/select-root.json'; +import SelectTriggerApiJsonPageContent from '../../api/select-trigger.json'; + +export default function Page(props) { + const { userLanguage, ...other } = props; + return ; +} + +Page.getLayout = (page) => { + return {page}; +}; + +export const getStaticPaths = () => { + return { + paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], + fallback: false, // can also be true or 'blocking' + }; +}; + +export const getStaticProps = () => { + const SelectItemApiReq = require.context( + 'docs-base/translations/api-docs/select-item', + false, + /\.\/select-item.*.json$/, + ); + const SelectItemApiDescriptions = mapApiPageTranslations(SelectItemApiReq); + + const SelectPopupApiReq = require.context( + 'docs-base/translations/api-docs/select-popup', + false, + /\.\/select-popup.*.json$/, + ); + const SelectPopupApiDescriptions = mapApiPageTranslations(SelectPopupApiReq); + + const SelectPositionerApiReq = require.context( + 'docs-base/translations/api-docs/select-positioner', + false, + /\.\/select-positioner.*.json$/, + ); + const SelectPositionerApiDescriptions = mapApiPageTranslations(SelectPositionerApiReq); + + const SelectRootApiReq = require.context( + 'docs-base/translations/api-docs/select-root', + false, + /\.\/select-root.*.json$/, + ); + const SelectRootApiDescriptions = mapApiPageTranslations(SelectRootApiReq); + + const SelectTriggerApiReq = require.context( + 'docs-base/translations/api-docs/select-trigger', + false, + /\.\/select-trigger.*.json$/, + ); + const SelectTriggerApiDescriptions = mapApiPageTranslations(SelectTriggerApiReq); + + return { + props: { + componentsApiDescriptions: { + SelectItem: SelectItemApiDescriptions, + SelectPopup: SelectPopupApiDescriptions, + SelectPositioner: SelectPositionerApiDescriptions, + SelectRoot: SelectRootApiDescriptions, + SelectTrigger: SelectTriggerApiDescriptions, + }, + componentsApiPageContents: { + SelectItem: SelectItemApiJsonPageContent, + SelectPopup: SelectPopupApiJsonPageContent, + SelectPositioner: SelectPositionerApiJsonPageContent, + SelectRoot: SelectRootApiJsonPageContent, + SelectTrigger: SelectTriggerApiJsonPageContent, + }, + hooksApiDescriptions: {}, + hooksApiPageContents: {}, + }, + }; +}; diff --git a/docs/translations/api-docs/select-item/select-item.json b/docs/translations/api-docs/select-item/select-item.json new file mode 100644 index 000000000..46541ccb2 --- /dev/null +++ b/docs/translations/api-docs/select-item/select-item.json @@ -0,0 +1,15 @@ +{ + "componentDescription": "An unstyled menu item to be used within a Menu.", + "propDescriptions": { + "closeOnClick": { + "description": "If true, the menu will close when the menu item is clicked." + }, + "disabled": { "description": "If true, the menu item will be disabled." }, + "id": { "description": "The id of the menu item." }, + "label": { + "description": "A text representation of the menu item's content. Used for keyboard text navigation matching." + }, + "onClick": { "description": "The click handler for the menu item." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-popup/select-popup.json b/docs/translations/api-docs/select-popup/select-popup.json new file mode 100644 index 000000000..4a1c0a206 --- /dev/null +++ b/docs/translations/api-docs/select-popup/select-popup.json @@ -0,0 +1,11 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "id": { "description": "The id of the popup element." }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-positioner/select-positioner.json b/docs/translations/api-docs/select-positioner/select-positioner.json new file mode 100644 index 000000000..cfd19a4bc --- /dev/null +++ b/docs/translations/api-docs/select-positioner/select-positioner.json @@ -0,0 +1,43 @@ +{ + "componentDescription": "Renders the element that positions the Select popup.", + "propDescriptions": { + "alignment": { + "description": "The alignment of the Menu element to the anchor element along its cross axis." + }, + "alignmentOffset": { + "description": "The offset of the Menu element along its alignment axis." + }, + "anchor": { "description": "The anchor element to which the Menu popup will be placed at." }, + "arrowPadding": { + "description": "Determines the padding between the arrow and the Menu popup's edges. Useful when the popover popup has rounded corners via border-radius." + }, + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "collisionBoundary": { + "description": "The boundary that the Menu element should be constrained to." + }, + "collisionPadding": { "description": "The padding of the collision boundary." }, + "container": { + "description": "The container element to which the Menu popup will be appended to." + }, + "hideWhenDetached": { + "description": "If true, the Menu will be hidden if it is detached from its anchor element due to differing clipping contexts." + }, + "keepMounted": { + "description": "Whether the menu popup remains mounted in the DOM while closed." + }, + "positionStrategy": { + "description": "The CSS position strategy for positioning the Menu popup element." + }, + "render": { "description": "A function to customize rendering of the component." }, + "side": { + "description": "The side of the anchor element that the Menu element should align to." + }, + "sideOffset": { "description": "The gap between the anchor element and the Menu element." }, + "sticky": { + "description": "If true, allow the Menu to remain in stuck view while the anchor element is scrolled out of view." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json new file mode 100644 index 000000000..bcc4b0296 --- /dev/null +++ b/docs/translations/api-docs/select-root/select-root.json @@ -0,0 +1,22 @@ +{ + "componentDescription": "", + "propDescriptions": { + "animated": { + "description": "If true, the Menu supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." + }, + "defaultOpen": { "description": "If true, the Menu is initially open." }, + "dir": { "description": "The direction of the Menu (left-to-right or right-to-left)." }, + "disabled": { "description": "If true, the Menu is disabled." }, + "loop": { + "description": "If true, using keyboard navigation will wrap focus to the other end of the list once the end is reached." + }, + "onOpenChange": { + "description": "Callback fired when the component requests to be opened or closed." + }, + "open": { + "description": "Allows to control whether the dropdown is open. This is a controlled counterpart of defaultOpen." + }, + "orientation": { "description": "The orientation of the Menu (horizontal or vertical)." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-trigger/select-trigger.json b/docs/translations/api-docs/select-trigger/select-trigger.json new file mode 100644 index 000000000..814aa3e49 --- /dev/null +++ b/docs/translations/api-docs/select-trigger/select-trigger.json @@ -0,0 +1,15 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "disabled": { "description": "If true, the component is disabled." }, + "focusableWhenDisabled": { + "description": "If true, allows a disabled button to receive focus." + }, + "label": { "description": "Label of the button" }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/use-select-item/use-select-item.json b/docs/translations/api-docs/use-select-item/use-select-item.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-select-item/use-select-item.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-positioner/use-select-positioner.json b/docs/translations/api-docs/use-select-positioner/use-select-positioner.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-select-positioner/use-select-positioner.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-root/use-select-root.json b/docs/translations/api-docs/use-select-root/use-select-root.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-select-root/use-select-root.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-trigger/use-select-trigger.json b/docs/translations/api-docs/use-select-trigger/use-select-trigger.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-select-trigger/use-select-trigger.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/translations.json b/docs/translations/translations.json new file mode 100644 index 000000000..4058a0243 --- /dev/null +++ b/docs/translations/translations.json @@ -0,0 +1,244 @@ +{ + "adblock": "If you don't mind tech-related ads (no tracking or remarketing), and want to keep us running, please whitelist us in your blocker.", + "api-docs": { + "componentName": "Component name", + "componentsApi": "Components API", + "themeDefaultProps": "Theme default props", + "themeDefaultPropsDescription": "You can use {{muiName}} to change the default props of this component with the theme.", + "classes": "CSS classes", + "classesDescription": "These class names are useful for styling with CSS. They are applied to the component's slots when specific states are triggered.", + "className": "Class name", + "cssDescription": "The following class names are useful for styling with CSS (the state classes are marked).
To learn more, visit the component customization page.", + "css": "CSS", + "cssComponent": "As a CSS utility, the {{name}} component also supports all system properties. You can use them as props directly on the component.", + "default": "Default", + "defaultComponent": "Default component", + "defaultValue": "Default value", + "defaultHTMLTag": "Default HTML tag", + "demos": "Component demos", + "deprecated": "Deprecated", + "description": "Description", + "globalClass": "Global class", + "defaultClass": "Default class", + "hookName": "Hook name", + "hooksApi": "Hooks API", + "hooksNoParameters": "This hook does not accept any input parameters.", + "hooksPageDescription": "API reference docs for the {{name}} hook. Learn about the input parameters and other APIs of this exported module.", + "import": "Import", + "importDifference": "Learn about the difference by reading this guide on minimizing bundle size.", + "inheritance": "Inheritance", + "inheritanceDescription": "While not explicitly documented above, the props of the {{component}} component{{suffix}} are also available in {{name}}. You can take advantage of this to target nested components.", + "inheritanceSuffixTransition": " from react-transition-group", + "name": "Name", + "nativeElement": "native", + "overrideStyles": "You can override the style of the component using one of these customization options:\n", + "overrideStylesStyledComponent": "", + "pageDescription": "API reference docs for the React {{name}} component. Learn about the props, CSS, and other APIs of this exported module.", + "props": "Props", + "parameters": "Parameters", + "requires-ref": "This needs to be able to hold a ref.", + "returns": "Returns: ", + "returnValue": "Return value", + "refNotHeld": "The component cannot hold a ref.", + "refRootElement": "The ref is forwarded to the root element.", + "ruleName": "Rule name", + "signature": "Signature", + "slots": "Slots", + "spreadHint": "Props of the {{spreadHintElement}} component are also available.", + "state": "STATE", + "styleOverrides": "The name {{componentStyles.name}} can be used when providing default props or style overrides in the theme.", + "slotDescription": "To learn how to customize the slot, check out the Overriding component structure guide.", + "slotName": "Slot name", + "type": "Type", + "required": "Required", + "optional": "Optional", + "additional-info": { + "cssApi": "See CSS API below for more details.", + "sx": "See the `sx` page for more details.", + "slotsApi": "See Slots API below for more details.", + "joy-size": "To learn how to add custom sizes to the component, check out Themed components—Extend sizes.", + "joy-color": "To learn how to add your own colors, check out Themed components—Extend colors.", + "joy-variant": "To learn how to add your own variants, check out Themed components—Extend variants." + } + }, + "landingPageDescr": "A responsive landing page layout with many common sections.", + "landingPageTitle": "Landing page", + "searchButton": "Search…", + "algoliaSearch": "What are you looking for?", + "appFrame": { + "changeLanguage": "Change language", + "github": "GitHub repository", + "helpToTranslate": "Help to translate", + "openDrawer": "Open main navigation", + "skipToContent": "Skip to content", + "toggleSettings": "Toggle settings drawer" + }, + "backToTop": "Scroll back to top", + "blogDescr": "A sophisticated blog page layout. Markdown support is courtesy of markdown-to-jsx.", + "blogTitle": "Blog", + "bundleSize": "Bundle size", + "bundleSizeTooltip": "Scroll down to 'Exports Analysis' for a more detailed report.", + "cancel": "Cancel", + "cdn": "or use a CDN.", + "checkoutDescr": "A step-by-step checkout page layout. Adapt the number of steps to suit your needs, or make steps optional.", + "checkoutTitle": "Checkout", + "clickToCopy": "Click to copy", + "close": "Close", + "codesandbox": "Edit in CodeSandbox", + "copied": "Copied", + "copiedSource": "The source code has been copied to your clipboard.", + "copiedSourceLink": "Link to the source code has been copied to your clipboard.", + "copySource": "Copy the source", + "copySourceLinkJS": "Copy link to JavaScript source", + "copySourceLinkTS": "Copy link to TypeScript source", + "dashboardDescr": "Contains a taskbar and a mini variant drawer. The chart is courtesy of Recharts.", + "dashboardTitle": "Dashboard", + "decreaseSpacing": "decrease spacing", + "demoToolbarLabel": "demo source", + "demoStylingSelectSystem": "MUI System", + "demoStylingSelectTailwind": "Tailwind CSS", + "demoStylingSelectCSS": "Plain CSS", + "diamondSponsors": "Diamond sponsors", + "becomeADiamondSponsor": "Become a Diamond sponsor", + "diamondSponsorVacancies": "One spot left!", + "editorHint": "Press Enter to start editing", + "editPage": "Edit this page", + "emojiLove": "Love", + "emojiWarning": "Warning", + "expandAll": "Expand all", + "feedbackCommentLabel": "Comment", + "feedbackFailed": "Couldn't submit feedback. Please try again later.", + "feedbackMessage": "Was this page helpful?", + "feedbackMessageDown": "How can we improve this page? (optional)", + "feedbackMessageUp": "What did you like about this page? (optional)", + "feedbackSectionSpecific": "How can we improve the {{sectionName}} section? (optional)", + "feedbackMessageToGitHub": { + "usecases": "If something is broken or if you need a reply to a problem you've encountered, please", + "reasonWhy": "Otherwise, the team won't be able to answer back or ask for more information.", + "callToAction": { + "link": "open an issue instead." + } + }, + "feedbackNo": "No", + "feedbackSubmitted": "Feedback submitted", + "feedbackYes": "Yes", + "footerCompany": "Company", + "goToHome": "go to homepage", + "getProfessionalSupport": "Get Professional Support", + "getStarted": "Get Started", + "githubLabel": "Feedback", + "headTitle": "MUI: A popular React UI framework", + "hideFullSource": "Collapse code", + "hideSource": "Hide code", + "homeQuickWord": "A quick word from our sponsors:", + "increaseSpacing": "increase spacing", + "initialFocusLabel": "A generic container that is programmatically focused to test keyboard navigation of our components.", + "installation": "Installation", + "installButton": "Read installation docs", + "installDescr": "Install MUI's source files via npm. We take care of injecting the CSS needed.", + "joinThese": "Join these and other great organizations!", + "JS": "JavaScript", + "letUsKnow": "Let us know!", + "likeMui": "Help us keep running", + "loadFont": "Load the default Roboto font.", + "mainNavigation": "documentation", + "newest": "Newest", + "openDrawer": "Open documentation navigation", + "or": "or", + "pageTOC": "Page table of contents", + "praise": "Praise for MUI", + "praiseDescr": "Here's what some of our users are saying.", + "pricingDescr": "Quickly build an effective pricing table for your potential customers.", + "pricingTitle": "Pricing", + "resetDemo": "Reset demo", + "resetDensity": "Reset density", + "resetFocus": "Reset focus to test keyboard navigation", + "searchIcons": { + "learnMore": "Learn more about the import" + }, + "seeMore": "See more", + "settings": { + "color": "Color", + "dark": "Dark", + "direction": "Direction", + "editWebsiteColors": "Edit website colors", + "light": "Light", + "ltr": "Left to right", + "mode": "Mode", + "rtl": "Right to left", + "settings": "Settings", + "system": "System", + "language": "Language" + }, + "showFullSource": "Expand code", + "showJSSource": "Show JavaScript source", + "showSource": "Show code", + "showTSSource": "Show TypeScript source", + "signInDescr": "A simple sign-in page using text fields, buttons, checkboxes, links, and more.", + "signInSideDescr": "A simple sign-in page with a two-column layout using text fields, buttons, and more.", + "signInSideTitle": "Sign-in side", + "signInTitle": "Sign-in", + "signUpDescr": "A simple sign-up page using text fields, buttons, checkboxes, links, and more.", + "signUpTitle": "Sign-up", + "sourceCode": "Source code", + "spacingUnit": "Spacing unit", + "stackblitz": "Edit in StackBlitz", + "stars": "GitHub stars", + "stickyFooterDescr": "Attach a footer to the bottom of the viewport when page content is short.", + "stickyFooterTitle": "Sticky footer", + "strapline": "MUI provides a simple, customizable, and accessible library of React components. Follow your own design system, or start with Material Design.", + "submit": "Submit", + "tableOfContents": "Contents", + "thanks": "Thank you!", + "themes": "Premium themes", + "themesButton": "Browse themes", + "themesDescr": "Take your project to the next level with premium themes from our store – all built on MUI.", + "toggleNotifications": "Toggle notifications panel", + "toggleRTL": "Toggle right-to-left/left-to-right", + "traffic": "Traffic", + "TS": "TypeScript", + "v5IsOut": "🎉 v5 release candidate is out! Head to the", + "v5docsLink": "v5 documentation", + "v5startAdoption": "to get started.", + "unreadNotifications": "unread notifications", + "usage": "Usage", + "usageButton": "Explore the docs", + "usageDescr": "MUI components work without any additional setup, and don't pollute the global scope.", + "useDarkTheme": "Use dark theme", + "useHighDensity": "Apply higher density via props", + "usingMui": "Are you using MUI?", + "viewGitHub": "View the source on GitHub", + "visit": "Visit the website", + "whosUsing": "Who's using MUI?", + "pages": { + "/base-ui/getting-started-group": "Getting started", + "/base-ui/getting-started": "Overview", + "/base-ui/getting-started/quickstart": "Quickstart", + "/base-ui/getting-started/usage": "Usage", + "/base-ui/getting-started/accessibility": "Accessibility", + "/base-ui/getting-started/support": "Support", + "/base-ui/react-": "Components", + "/base-ui/all-components": "All components", + "inputs": "Inputs", + "/base-ui/react-checkbox": "Checkbox", + "/base-ui/react-number-field": "Number Field", + "/base-ui/react-select": "Select", + "/base-ui/react-slider": "Slider", + "/base-ui/react-switch": "Switch", + "data-display": "Data display", + "/base-ui/react-popover": "Popover", + "/base-ui/react-preview-card": "Preview Card", + "/base-ui/react-tooltip": "Tooltip", + "/base-ui/react-field": "Field", + "/base-ui/react-fieldset": "Fieldset", + "feedback": "Feedback", + "/base-ui/react-alert-dialog": "Alert Dialog", + "/base-ui/react-dialog": "Dialog", + "/base-ui/react-progress": "Progress", + "navigation": "Navigation", + "/base-ui/react-menu": "Menu", + "/base-ui/react-tabs": "Tabs", + "/base-ui/guides": "How-to guides", + "/base-ui/guides/next-js-app-router": "Next.js App Router" + } +} diff --git a/packages/mui-base/src/Select/Item/SelectItem.tsx b/packages/mui-base/src/Select/Item/SelectItem.tsx new file mode 100644 index 000000000..96f67b393 --- /dev/null +++ b/packages/mui-base/src/Select/Item/SelectItem.tsx @@ -0,0 +1,174 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useListItem } from '@floating-ui/react'; +import { useSelectItem } from './useSelectItem'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useId } from '../../utils/useId'; +import type { BaseUIComponentProps, GenericHTMLProps } from '../../utils/types'; +import { useForkRef } from '../../utils/useForkRef'; + +const InnerSelectItem = React.memo( + React.forwardRef(function InnerSelectItem( + props: InnerSelectItemProps, + forwardedRef: React.ForwardedRef, + ) { + const { + className, + closeOnClick = true, + disabled = false, + highlighted, + id, + propGetter, + render, + treatMouseupAsClick, + ...otherProps + } = props; + + const { getRootProps } = useSelectItem({ + closeOnClick, + disabled, + highlighted, + id, + ref: forwardedRef, + treatMouseupAsClick, + }); + + const ownerState: SelectItem.OwnerState = React.useMemo( + () => ({ disabled, highlighted }), + [disabled, highlighted], + ); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'div', + className, + ownerState, + propGetter: (externalProps) => propGetter(getRootProps(externalProps)), + extraProps: otherProps, + }); + + return renderElement(); + }), +); + +/** + * An unstyled menu item to be used within a Menu. + * + * Demos: + * + * - [Menu](https://mui.com/base-ui/react-menu/) + * + * API: + * + * - [SelectItem API](https://mui.com/base-ui/react-menu/components-api/#menu-item) + */ +const SelectItem = React.forwardRef(function SelectItem( + props: SelectItem.Props, + forwardedRef: React.ForwardedRef, +) { + const { id: idProp, label, ...other } = props; + + const { getItemProps, activeIndex, clickAndDragEnabled } = useSelectRootContext(); + + const itemRef = React.useRef(null); + const listItem = useListItem({ label: label ?? itemRef.current?.innerText }); + const mergedRef = useForkRef(forwardedRef, listItem.ref, itemRef); + + const id = useId(idProp); + + const highlighted = listItem.index === activeIndex; + + // This wrapper component is used as a performance optimization. + // SelectItem reads the context and re-renders the actual SelectItem + // only when it needs to. + + return ( + + ); +}); + +interface InnerSelectItemProps extends SelectItem.Props { + highlighted: boolean; + propGetter: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + treatMouseupAsClick: boolean; +} + +namespace SelectItem { + export type OwnerState = { + disabled: boolean; + highlighted: boolean; + }; + + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + children?: React.ReactNode; + /** + * The click handler for the menu item. + */ + onClick?: React.MouseEventHandler; + /** + * If `true`, the menu item will be disabled. + * @default false + */ + disabled?: boolean; + /** + * A text representation of the menu item's content. + * Used for keyboard text navigation matching. + */ + label?: string; + /** + * The id of the menu item. + */ + id?: string; + /** + * If `true`, the menu will close when the menu item is clicked. + * + * @default true + */ + closeOnClick?: boolean; + } +} + +SelectItem.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * If `true`, the menu will close when the menu item is clicked. + * + * @default true + */ + closeOnClick: PropTypes.bool, + /** + * If `true`, the menu item will be disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * The id of the menu item. + */ + id: PropTypes.string, + /** + * A text representation of the menu item's content. + * Used for keyboard text navigation matching. + */ + label: PropTypes.string, + /** + * The click handler for the menu item. + */ + onClick: PropTypes.func, +} as any; + +export { SelectItem }; diff --git a/packages/mui-base/src/Select/Item/useSelectItem.ts b/packages/mui-base/src/Select/Item/useSelectItem.ts new file mode 100644 index 000000000..312f88158 --- /dev/null +++ b/packages/mui-base/src/Select/Item/useSelectItem.ts @@ -0,0 +1,85 @@ +'use client'; +import * as React from 'react'; +import type { GenericHTMLProps } from '../../utils/types'; +import { useButton } from '../../useButton'; +import { mergeReactProps } from '../../utils/mergeReactProps'; + +/** + * + * API: + * + * - [useSelectItem API](https://mui.com/base-ui/api/use-select-item/) + */ +export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.ReturnValue { + const { disabled = false, highlighted, id, ref: externalRef, treatMouseupAsClick } = params; + + const { getRootProps: getButtonProps, rootRef: mergedRef } = useButton({ + disabled, + focusableWhenDisabled: true, + rootRef: externalRef, + }); + + const getRootProps = React.useCallback( + (externalProps?: GenericHTMLProps): GenericHTMLProps => { + return getButtonProps( + mergeReactProps(externalProps, { + 'data-handle-mouseup': treatMouseupAsClick || undefined, + id, + role: 'SelectItem', + tabIndex: highlighted ? 0 : -1, + }), + ); + }, + [getButtonProps, highlighted, id, treatMouseupAsClick], + ); + + return React.useMemo( + () => ({ + getRootProps, + rootRef: mergedRef, + }), + [getRootProps, mergedRef], + ); +} + +export namespace useSelectItem { + export interface Parameters { + /** + * If `true`, the menu will close when the menu item is clicked. + */ + closeOnClick: boolean; + /** + * If `true`, the menu item will be disabled. + */ + disabled: boolean; + /** + * Determines if the menu item is highlighted. + */ + highlighted: boolean; + /** + * The id of the menu item. + */ + id: string | undefined; + /** + * The ref of the trigger element. + */ + ref?: React.Ref; + /** + * If `true`, the menu item will listen for mouseup events and treat them as clicks. + */ + treatMouseupAsClick: boolean; + } + + export interface ReturnValue { + /** + * Resolver for the root slot's props. + * @param externalProps event handlers for the root slot + * @returns props that should be spread on the root slot + */ + getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + /** + * The ref to the component's root DOM element. + */ + rootRef: React.RefCallback | null; + } +} diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.tsx new file mode 100644 index 000000000..132c641da --- /dev/null +++ b/packages/mui-base/src/Select/Popup/SelectPopup.tsx @@ -0,0 +1,97 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import type { Side } from '@floating-ui/react'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useForkRef } from '../../utils/useForkRef'; +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; + +const customStyleHookMapping: CustomStyleHookMapping = { + ...commonStyleHooks, + entering(value) { + return value ? { 'data-menu-entering': '' } : null; + }, + exiting(value) { + return value ? { 'data-menu-exiting': '' } : null; + }, +}; + +const SelectPopup = React.forwardRef(function SelectPopup( + props: SelectPopup.Props, + forwardedRef: React.ForwardedRef, +) { + const { render, className, ...other } = props; + const { open, popupRef, transitionStatus } = useSelectRootContext(); + const { side, alignment } = useSelectPositionerContext(); + + const mergedRef = useForkRef(forwardedRef, popupRef); + + const ownerState: SelectPopup.OwnerState = React.useMemo( + () => ({ + entering: transitionStatus === 'entering', + exiting: transitionStatus === 'exiting', + side, + alignment, + open, + }), + [transitionStatus, side, alignment, open], + ); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'div', + className, + ownerState, + extraProps: other, + customStyleHookMapping, + ref: mergedRef, + }); + + return renderElement(); +}); + +namespace SelectPopup { + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + children?: React.ReactNode; + /** + * The id of the popup element. + */ + id?: string; + } + + export interface OwnerState { + entering: boolean; + exiting: boolean; + side: Side; + alignment: 'start' | 'end' | 'center'; + open: boolean; + } +} + +SelectPopup.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * The id of the popup element. + */ + id: PropTypes.string, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectPopup }; diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx new file mode 100644 index 000000000..73dee4853 --- /dev/null +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -0,0 +1,274 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { + FloatingFocusManager, + FloatingList, + FloatingNode, + FloatingPortal, + Side, + useFloatingNodeId, +} from '@floating-ui/react'; +import { SelectPositionerContext } from './SelectPositionerContext'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useForkRef } from '../../utils/useForkRef'; +import { useSelectPositioner } from './useSelectPositioner'; +import { HTMLElementType } from '../../utils/proptypes'; +import { BaseUIComponentProps, GenericHTMLProps } from '../../utils/types'; + +/** + * Renders the element that positions the Select popup. + * + * Demos: + * + * - [Menu](https://mui.com/base-ui/react-select/) + * + * API: + * + * - [SelectPositioner API](https://mui.com/base-ui/react-select/components-api/#select-positioner) + */ +const SelectPositioner = React.forwardRef(function SelectPositioner( + props: SelectPositioner.Props, + forwardedRef: React.ForwardedRef, +) { + const { + anchor, + positionStrategy = 'absolute', + className, + render, + keepMounted = false, + side = 'bottom', + alignment = 'center', + sideOffset = 0, + alignmentOffset = 0, + collisionBoundary, + collisionPadding = 5, + arrowPadding = 5, + hideWhenDetached = false, + sticky = false, + container, + ...otherProps + } = props; + + const { + open, + floatingRootContext, + getPositionerProps, + setPositionerElement, + itemDomElements, + itemLabels, + triggerElement, + mounted, + } = useSelectRootContext(); + + const nodeId = useFloatingNodeId(); + + const positioner = useSelectPositioner({ + anchor: anchor || triggerElement, + floatingRootContext, + positionStrategy, + container, + open, + mounted, + side, + sideOffset, + alignment, + alignmentOffset, + arrowPadding, + collisionBoundary, + collisionPadding, + hideWhenDetached, + sticky, + nodeId, + }); + + const ownerState: SelectPositioner.OwnerState = React.useMemo( + () => ({ + open, + side: positioner.side, + alignment: positioner.alignment, + }), + [open, positioner.side, positioner.alignment], + ); + + const contextValue: SelectPositionerContext = React.useMemo( + () => ({ + side: positioner.side, + alignment: positioner.alignment, + arrowRef: positioner.arrowRef, + arrowUncentered: positioner.arrowUncentered, + arrowStyles: positioner.arrowStyles, + floatingContext: positioner.floatingContext, + }), + [ + positioner.side, + positioner.alignment, + positioner.arrowRef, + positioner.arrowUncentered, + positioner.arrowStyles, + positioner.floatingContext, + ], + ); + + const mergedRef = useForkRef(forwardedRef, setPositionerElement); + + const { renderElement } = useComponentRenderer({ + propGetter: (externalProps: GenericHTMLProps) => + positioner.getPositionerProps(getPositionerProps(externalProps)), + render: render ?? 'div', + className, + ownerState, + customStyleHookMapping: commonStyleHooks, + ref: mergedRef, + extraProps: otherProps, + }); + + const shouldRender = keepMounted || mounted; + if (!shouldRender) { + return null; + } + + return ( + + + + + + {renderElement()} + + + + + + ); +}); + +export { SelectPositioner }; + +export namespace SelectPositioner { + export type OwnerState = { + open: boolean; + side: Side; + alignment: 'start' | 'end' | 'center'; + }; + + export interface Props + extends useSelectPositioner.SharedParameters, + BaseUIComponentProps<'div', OwnerState> {} +} + +SelectPositioner.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * The alignment of the Menu element to the anchor element along its cross axis. + * @default 'center' + */ + alignment: PropTypes.oneOf(['center', 'end', 'start']), + /** + * The offset of the Menu element along its alignment axis. + * @default 0 + */ + alignmentOffset: PropTypes.number, + /** + * The anchor element to which the Menu popup will be placed at. + */ + anchor: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + HTMLElementType, + PropTypes.object, + PropTypes.func, + ]), + /** + * Determines the padding between the arrow and the Menu popup's edges. Useful when the popover + * popup has rounded corners via `border-radius`. + * @default 5 + */ + arrowPadding: PropTypes.number, + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * The boundary that the Menu element should be constrained to. + * @default 'clippingAncestors' + */ + collisionBoundary: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + HTMLElementType, + PropTypes.arrayOf(HTMLElementType), + PropTypes.string, + PropTypes.shape({ + height: PropTypes.number, + width: PropTypes.number, + x: PropTypes.number, + y: PropTypes.number, + }), + ]), + /** + * The padding of the collision boundary. + * @default 5 + */ + collisionPadding: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + }), + ]), + /** + * The container element to which the Menu popup will be appended to. + */ + container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + HTMLElementType, + PropTypes.func, + ]), + /** + * If `true`, the Menu will be hidden if it is detached from its anchor element due to + * differing clipping contexts. + * @default false + */ + hideWhenDetached: PropTypes.bool, + /** + * Whether the menu popup remains mounted in the DOM while closed. + * @default false + */ + keepMounted: PropTypes.bool, + /** + * The CSS position strategy for positioning the Menu popup element. + * @default 'absolute' + */ + positionStrategy: PropTypes.oneOf(['absolute', 'fixed']), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + /** + * The side of the anchor element that the Menu element should align to. + * @default 'bottom' + */ + side: PropTypes.oneOf(['bottom', 'left', 'right', 'top']), + /** + * The gap between the anchor element and the Menu element. + * @default 0 + */ + sideOffset: PropTypes.number, + /** + * If `true`, allow the Menu to remain in stuck view while the anchor element is scrolled out + * of view. + * @default false + */ + sticky: PropTypes.bool, +} as any; diff --git a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts new file mode 100644 index 000000000..e403ba580 --- /dev/null +++ b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts @@ -0,0 +1,31 @@ +'use client'; +import * as React from 'react'; +import type { Side } from '@floating-ui/react'; + +export interface SelectPositionerContext { + /** + * The side of the anchor element the popup is positioned relative to. + */ + side: Side; + /** + * The alignment of the anchor element the popup is positioned relative to. + */ + alignment: 'start' | 'end' | 'center'; + arrowRef: React.MutableRefObject; + arrowUncentered: boolean; + arrowStyles: React.CSSProperties; +} + +export const SelectPositionerContext = React.createContext(null); + +if (process.env.NODE_ENV !== 'production') { + SelectPositionerContext.displayName = 'SelectPositionerContext'; +} + +export function useSelectPositionerContext() { + const context = React.useContext(SelectPositionerContext); + if (context === null) { + throw new Error(' must be used within the component'); + } + return context; +} diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx new file mode 100644 index 000000000..0bcdeaa0d --- /dev/null +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -0,0 +1,203 @@ +import * as React from 'react'; +import type { + VirtualElement, + Side, + Padding, + Boundary, + FloatingRootContext, + FloatingContext, +} from '@floating-ui/react'; +import type { GenericHTMLProps } from '../../utils/types'; +import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +/** + * + * API: + * + * - [useSelectPositioner API](https://mui.com/base-ui/api/use-select-positioner/) + */ +export function useSelectPositioner( + params: useSelectPositioner.Parameters, +): useSelectPositioner.ReturnValue { + const { open = false, keepMounted } = params; + + const { + positionerStyles, + arrowStyles, + hidden, + arrowRef, + arrowUncentered, + renderedSide, + renderedAlignment, + positionerContext: floatingContext, + } = useAnchorPositioning(params); + + const getPositionerProps: useSelectPositioner.ReturnValue['getPositionerProps'] = + React.useCallback( + (externalProps = {}) => { + const hiddenStyles: React.CSSProperties = {}; + + if ((keepMounted && !open) || hidden) { + hiddenStyles.pointerEvents = 'none'; + } + + return mergeReactProps(externalProps, { + style: { + ...positionerStyles, + ...hiddenStyles, + zIndex: 2147483647, // max z-index + }, + 'aria-hidden': !open || undefined, + }); + }, + [positionerStyles, open, keepMounted, hidden], + ); + + return React.useMemo( + () => ({ + getPositionerProps, + arrowRef, + arrowUncentered, + arrowStyles, + side: renderedSide, + alignment: renderedAlignment, + floatingContext, + }), + [ + getPositionerProps, + arrowRef, + arrowUncentered, + arrowStyles, + renderedSide, + renderedAlignment, + floatingContext, + ], + ); +} + +export namespace useSelectPositioner { + export interface SharedParameters { + /** + * If `true`, the Menu is open. + */ + open?: boolean; + /** + * The anchor element to which the Menu popup will be placed at. + */ + anchor?: + | Element + | null + | VirtualElement + | React.MutableRefObject + | (() => Element | VirtualElement | null); + /** + * The CSS position strategy for positioning the Menu popup element. + * @default 'absolute' + */ + positionStrategy?: 'absolute' | 'fixed'; + /** + * The container element to which the Menu popup will be appended to. + */ + container?: HTMLElement | null | React.MutableRefObject; + /** + * The side of the anchor element that the Menu element should align to. + * @default 'bottom' + */ + side?: Side; + /** + * The gap between the anchor element and the Menu element. + * @default 0 + */ + sideOffset?: number; + /** + * The alignment of the Menu element to the anchor element along its cross axis. + * @default 'center' + */ + alignment?: 'start' | 'end' | 'center'; + /** + * The offset of the Menu element along its alignment axis. + * @default 0 + */ + alignmentOffset?: number; + /** + * The boundary that the Menu element should be constrained to. + * @default 'clippingAncestors' + */ + collisionBoundary?: Boundary; + /** + * The padding of the collision boundary. + * @default 5 + */ + collisionPadding?: Padding; + /** + * If `true`, the Menu will be hidden if it is detached from its anchor element due to + * differing clipping contexts. + * @default false + */ + hideWhenDetached?: boolean; + /** + * Whether the menu popup remains mounted in the DOM while closed. + * @default false + */ + keepMounted?: boolean; + /** + * If `true`, allow the Menu to remain in stuck view while the anchor element is scrolled out + * of view. + * @default false + */ + sticky?: boolean; + /** + * Determines the padding between the arrow and the Menu popup's edges. Useful when the popover + * popup has rounded corners via `border-radius`. + * @default 5 + */ + arrowPadding?: number; + } + + export interface Parameters extends SharedParameters { + /** + * If `true`, the Menu is mounted. + * @default true + */ + mounted?: boolean; + /** + * The Menu root context. + */ + floatingRootContext?: FloatingRootContext; + /** + * Floating node id. + */ + nodeId?: string; + } + + export interface ReturnValue { + /** + * Props to spread on the Menu positioner element. + */ + getPositionerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + /** + * The ref of the Menu arrow element. + */ + arrowRef: React.MutableRefObject; + /** + * Determines if the arrow cannot be centered. + */ + arrowUncentered: boolean; + /** + * The rendered side of the Menu element. + */ + side: 'top' | 'right' | 'bottom' | 'left'; + /** + * The rendered alignment of the Menu element. + */ + alignment: 'start' | 'end' | 'center'; + /** + * The styles to apply to the Menu arrow element. + */ + arrowStyles: React.CSSProperties; + /** + * The floating context. + */ + floatingContext: FloatingContext; + } +} diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx new file mode 100644 index 000000000..a6475f1f8 --- /dev/null +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -0,0 +1,151 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { SelectRootContext } from './SelectRootContext'; +import { MenuDirection, MenuOrientation, useSelectRoot } from './useSelectRoot'; + +function SelectRoot(props: SelectRoot.Props) { + const { + animated = true, + children, + defaultOpen = false, + disabled = false, + loop = true, + onOpenChange, + open, + orientation = 'vertical', + } = props; + + const selectRoot = useSelectRoot({ + animated, + disabled, + onOpenChange, + loop, + defaultOpen, + open, + orientation, + }); + + const [clickAndDragEnabled, setClickAndDragEnabled] = React.useState(false); + + const context: SelectRootContext = React.useMemo( + () => ({ + ...selectRoot, + disabled, + clickAndDragEnabled, + setClickAndDragEnabled, + }), + [selectRoot, disabled, clickAndDragEnabled, setClickAndDragEnabled], + ); + + return {children}; +} + +namespace SelectRoot { + export interface Props { + /** + * If `true`, the Menu supports CSS-based animations and transitions. + * It is kept in the DOM until the animation completes. + * + * @default true + */ + animated?: boolean; + children: React.ReactNode; + /** + * If `true`, the Menu is initially open. + * + * @default false + */ + defaultOpen?: boolean; + /** + * If `true`, using keyboard navigation will wrap focus to the other end of the list once the end is reached. + * @default true + */ + loop?: boolean; + /** + * Callback fired when the component requests to be opened or closed. + */ + onOpenChange?: (open: boolean, event: Event | undefined) => void; + /** + * Allows to control whether the dropdown is open. + * This is a controlled counterpart of `defaultOpen`. + */ + open?: boolean; + /** + * The orientation of the Menu (horizontal or vertical). + * + * @default 'vertical' + */ + orientation?: MenuOrientation; + /** + * The direction of the Menu (left-to-right or right-to-left). + * + * @default 'ltr' + */ + dir?: MenuDirection; + /** + * If `true`, the Menu is disabled. + * + * @default false + */ + disabled?: boolean; + } +} + +SelectRoot.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * If `true`, the Menu supports CSS-based animations and transitions. + * It is kept in the DOM until the animation completes. + * + * @default true + */ + animated: PropTypes.bool, + /** + * @ignore + */ + children: PropTypes.node, + /** + * If `true`, the Menu is initially open. + * + * @default false + */ + defaultOpen: PropTypes.bool, + /** + * The direction of the Menu (left-to-right or right-to-left). + * + * @default 'ltr' + */ + dir: PropTypes.oneOf(['ltr', 'rtl']), + /** + * If `true`, the Menu is disabled. + * + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, using keyboard navigation will wrap focus to the other end of the list once the end is reached. + * @default true + */ + loop: PropTypes.bool, + /** + * Callback fired when the component requests to be opened or closed. + */ + onOpenChange: PropTypes.func, + /** + * Allows to control whether the dropdown is open. + * This is a controlled counterpart of `defaultOpen`. + */ + open: PropTypes.bool, + /** + * The orientation of the Menu (horizontal or vertical). + * + * @default 'vertical' + */ + orientation: PropTypes.oneOf(['horizontal', 'vertical']), +} as any; + +export { SelectRoot }; diff --git a/packages/mui-base/src/Select/Root/SelectRootContext.ts b/packages/mui-base/src/Select/Root/SelectRootContext.ts new file mode 100644 index 000000000..7ab12e0cc --- /dev/null +++ b/packages/mui-base/src/Select/Root/SelectRootContext.ts @@ -0,0 +1,23 @@ +'use client'; +import * as React from 'react'; +import type { useSelectRoot } from './useSelectRoot'; + +export interface SelectRootContext extends useSelectRoot.ReturnValue { + disabled: boolean; + clickAndDragEnabled: boolean; + setClickAndDragEnabled: React.Dispatch>; +} + +export const SelectRootContext = React.createContext(null); + +if (process.env.NODE_ENV !== 'production') { + SelectRootContext.displayName = 'SelectRootContext'; +} + +export function useSelectRootContext() { + const context = React.useContext(SelectRootContext); + if (context === null) { + throw new Error('Base UI: SelectRootContext is not defined.'); + } + return context; +} diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx new file mode 100644 index 000000000..ac7441044 --- /dev/null +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -0,0 +1,222 @@ +'use client'; +import * as React from 'react'; +import { + useClick, + useDismiss, + useFloatingRootContext, + useInteractions, + useListNavigation, + useRole, + useTypeahead, + type FloatingRootContext, +} from '@floating-ui/react'; +import type { GenericHTMLProps } from '../../utils/types'; +import { useTransitionStatus } from '../../utils/useTransitionStatus'; +import { useEventCallback } from '../../utils/useEventCallback'; +import { useAnimationsFinished } from '../../utils/useAnimationsFinished'; +import { useControlled } from '../../utils/useControlled'; + +const EMPTY_ARRAY: never[] = []; + +/** + * + * API: + * + * - [useSelectRoot API](https://mui.com/base-ui/api/use-select-root/) + */ +export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.ReturnValue { + const { + animated, + open: openParam, + defaultOpen, + onOpenChange, + orientation, + disabled, + loop, + } = params; + + const [triggerElement, setTriggerElement] = React.useState(null); + const [positionerElement, setPositionerElement] = React.useState(null); + const popupRef = React.useRef(null); + const [activeIndex, setActiveIndex] = React.useState(null); + const [selectedIndex, setSelectedIndex] = React.useState(null); + + const [open, setOpenUnwrapped] = useControlled({ + controlled: openParam, + default: defaultOpen, + name: 'useSelectRoot', + state: 'open', + }); + + const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, animated); + + const runOnceAnimationsFinish = useAnimationsFinished(popupRef); + const setOpen = useEventCallback((nextOpen: boolean, event?: Event) => { + onOpenChange?.(nextOpen, event); + setOpenUnwrapped(nextOpen); + if (!nextOpen) { + if (animated) { + runOnceAnimationsFinish(() => setMounted(false)); + } else { + setMounted(false); + } + } + }); + + const floatingRootContext = useFloatingRootContext({ + elements: { + reference: triggerElement, + floating: positionerElement, + }, + open, + onOpenChange: setOpen, + }); + + const click = useClick(floatingRootContext, { + enabled: !disabled, + event: 'mousedown', + }); + + const dismiss = useDismiss(floatingRootContext); + + const role = useRole(floatingRootContext, { + role: 'select', + }); + + const itemDomElements = React.useRef<(HTMLElement | null)[]>([]); + const itemLabels = React.useRef<(string | null)[]>([]); + + const listNavigation = useListNavigation(floatingRootContext, { + enabled: !disabled, + listRef: itemDomElements, + activeIndex, + selectedIndex, + loop, + orientation, + disabledIndices: EMPTY_ARRAY, + onNavigate: setActiveIndex, + }); + + const typeahead = useTypeahead(floatingRootContext, { + listRef: itemLabels, + activeIndex, + resetMs: 350, + onMatch: open ? setActiveIndex : undefined, + }); + + const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ + click, + dismiss, + role, + listNavigation, + typeahead, + ]); + + const getTriggerProps = React.useCallback( + (externalProps?: GenericHTMLProps) => getReferenceProps(externalProps), + [getReferenceProps], + ); + + const getPositionerProps = React.useCallback( + (externalProps?: GenericHTMLProps) => getFloatingProps(externalProps), + [getFloatingProps], + ); + + return React.useMemo( + () => ({ + activeIndex, + setActiveIndex, + selectedIndex, + setSelectedIndex, + floatingRootContext, + triggerElement, + setTriggerElement, + getTriggerProps, + setPositionerElement, + getPositionerProps, + getItemProps, + itemDomElements, + itemLabels, + mounted, + transitionStatus, + popupRef, + open, + setOpen, + }), + [ + activeIndex, + selectedIndex, + floatingRootContext, + triggerElement, + getTriggerProps, + getPositionerProps, + getItemProps, + itemDomElements, + itemLabels, + mounted, + transitionStatus, + open, + setOpen, + ], + ); +} + +export type MenuOrientation = 'horizontal' | 'vertical'; + +export type MenuDirection = 'ltr' | 'rtl'; + +export namespace useSelectRoot { + export interface Parameters { + /** + * If `true`, the Menu supports CSS-based animations and transitions. + * It is kept in the DOM until the animation completes. + */ + animated: boolean; + /** + * Allows to control whether the Menu is open. + * This is a controlled counterpart of `defaultOpen`. + */ + open: boolean | undefined; + /** + * Callback fired when the component requests to be opened or closed. + */ + onOpenChange: ((open: boolean, event: Event | undefined) => void) | undefined; + /** + * If `true`, the Menu is initially open. + */ + defaultOpen: boolean; + /** + * If `true`, using keyboard navigation will wrap focus to the other end of the list once the end is reached. + */ + loop: boolean; + /** + * The orientation of the Menu (horizontal or vertical). + */ + orientation: MenuOrientation; + /** + * If `true`, the Menu is disabled. + */ + disabled: boolean; + } + + export interface ReturnValue { + activeIndex: number | null; + setActiveIndex: React.Dispatch>; + selectedIndex: number | null; + setSelectedIndex: React.Dispatch>; + floatingRootContext: FloatingRootContext; + getItemProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + getPositionerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + itemDomElements: React.MutableRefObject<(HTMLElement | null)[]>; + itemLabels: React.MutableRefObject<(string | null)[]>; + mounted: boolean; + open: boolean; + popupRef: React.RefObject; + setOpen: (open: boolean, event: Event | undefined) => void; + setPositionerElement: (element: HTMLElement | null) => void; + setTriggerElement: (element: HTMLElement | null) => void; + transitionStatus: 'entering' | 'exiting' | undefined; + triggerElement: HTMLElement | null; + } +} diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx new file mode 100644 index 000000000..53a71ecf7 --- /dev/null +++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx @@ -0,0 +1,105 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useSelectTrigger } from './useSelectTrigger'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { BaseUIComponentProps } from '../../utils/types'; + +const SelectTrigger = React.forwardRef(function SelectTrigger( + props: SelectTrigger.Props, + forwardedRef: React.ForwardedRef, +) { + const { render, className, disabled = false, label, ...other } = props; + + const { + getTriggerProps, + disabled: menuDisabled, + setTriggerElement, + open, + setOpen, + setClickAndDragEnabled, + } = useSelectRootContext(); + + const { getRootProps } = useSelectTrigger({ + disabled: disabled || menuDisabled, + rootRef: forwardedRef, + setTriggerElement, + open, + setOpen, + setClickAndDragEnabled, + }); + + const ownerState: SelectTrigger.OwnerState = React.useMemo(() => ({ open }), [open]); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'button', + className, + ownerState, + propGetter: (externalProps) => getTriggerProps(getRootProps(externalProps)), + customStyleHookMapping: commonStyleHooks, + extraProps: other, + }); + + return renderElement(); +}); + +namespace SelectTrigger { + export interface Props extends BaseUIComponentProps<'button', OwnerState> { + children?: React.ReactNode; + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * If `true`, allows a disabled button to receive focus. + * @default false + */ + focusableWhenDisabled?: boolean; + /** + * Label of the button + */ + label?: string; + } + + export type OwnerState = { + open: boolean; + }; +} + +SelectTrigger.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * If `true`, the component is disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, allows a disabled button to receive focus. + * @default false + */ + focusableWhenDisabled: PropTypes.bool, + /** + * Label of the button + */ + label: PropTypes.string, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectTrigger }; diff --git a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts new file mode 100644 index 000000000..03ed6bf73 --- /dev/null +++ b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts @@ -0,0 +1,138 @@ +'use client'; +import * as React from 'react'; +import { useButton } from '../../useButton/useButton'; +import type { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { ownerDocument } from '../../utils/owner'; +import { useForkRef } from '../../utils/useForkRef'; + +/** + * + * API: + * + * - [useSelectTrigger API](https://mui.com/base-ui/api/use-select-trigger/) + */ +export function useSelectTrigger( + parameters: useSelectTrigger.Parameters, +): useSelectTrigger.ReturnValue { + const { + disabled = false, + rootRef: externalRef, + open, + setOpen, + setTriggerElement, + setClickAndDragEnabled, + } = parameters; + + const triggerRef = React.useRef(null); + + const mergedRef = useForkRef(externalRef, triggerRef); + + const { getRootProps: getButtonRootProps, rootRef: buttonRootRef } = useButton({ + disabled, + focusableWhenDisabled: false, + rootRef: mergedRef, + }); + + const handleRef = useForkRef(buttonRootRef, setTriggerElement); + const ignoreNextClickRef = React.useRef(false); + + const getRootProps = React.useCallback( + (externalProps?: GenericHTMLProps): GenericHTMLProps => { + return mergeReactProps( + externalProps, + { + tabIndex: 0, // this is needed to make the button focused after click in Safari + ref: handleRef, + onMouseDown: (event: MouseEvent) => { + if (open) { + return; + } + + // prevents closing the menu right after it was opened + ignoreNextClickRef.current = true; + event.preventDefault(); + + setClickAndDragEnabled(true); + const mousedownTarget = event.target as Element; + + function handleDocumentMouseUp(mouseUpEvent: MouseEvent) { + const mouseupTarget = mouseUpEvent.target as HTMLElement; + if (mouseupTarget?.dataset?.handleMouseup === 'true') { + mouseupTarget.click(); + } else if ( + mouseupTarget !== triggerRef.current && + !triggerRef.current?.contains(mouseupTarget) + ) { + setOpen(false, mouseUpEvent); + } + + setClickAndDragEnabled(false); + ownerDocument(mousedownTarget).removeEventListener('mouseup', handleDocumentMouseUp); + } + + ownerDocument(mousedownTarget).addEventListener('mouseup', handleDocumentMouseUp); + }, + onClick: () => { + if (ignoreNextClickRef.current) { + ignoreNextClickRef.current = false; + } + }, + }, + getButtonRootProps(), + ); + }, + [getButtonRootProps, handleRef, open, setOpen, setClickAndDragEnabled], + ); + + return React.useMemo( + () => ({ + getRootProps, + rootRef: handleRef, + }), + [getRootProps, handleRef], + ); +} + +export namespace useSelectTrigger { + export interface Parameters { + /** + * If `true`, the component is disabled. + * @default false + */ + disabled?: boolean; + /** + * The ref to the root element. + */ + rootRef?: React.Ref; + /** + * A callback to set the trigger element whenever it's mounted. + */ + setTriggerElement: (element: HTMLElement | null) => void; + /** + * If `true`, the Menu is open. + */ + open: boolean; + /** + * A callback to set the open state of the Menu. + */ + setOpen: (open: boolean, event: Event | undefined) => void; + /** + * A callback to enable/disable click and drag functionality. + */ + setClickAndDragEnabled: (enabled: boolean) => void; + } + + export interface ReturnValue { + /** + * Resolver for the root slot's props. + * @param externalProps props for the root slot + * @returns props that should be spread on the root slot + */ + getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + /** + * The ref to the root element. + */ + rootRef: React.RefCallback | null; + } +} diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts new file mode 100644 index 000000000..d868e61ee --- /dev/null +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -0,0 +1,5 @@ +export { SelectRoot } from './Root/SelectRoot'; +export { SelectTrigger } from './Trigger/SelectTrigger'; +export { SelectPositioner } from './Positioner/SelectPositioner'; +export { SelectPopup } from './Popup/SelectPopup'; +export { SelectItem } from './Item/SelectItem'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts new file mode 100644 index 000000000..3148fe195 --- /dev/null +++ b/packages/mui-base/src/Select/index.ts @@ -0,0 +1,5 @@ +export { SelectRoot as Root } from './Root/SelectRoot'; +export { SelectTrigger as Trigger } from './Trigger/SelectTrigger'; +export { SelectPositioner as Positioner } from './Positioner/SelectPositioner'; +export { SelectPopup as Popup } from './Popup/SelectPopup'; +export { SelectItem as Item } from './Item/SelectItem'; diff --git a/packages/mui-base/src/Select/utils/commonStyleHooks.ts b/packages/mui-base/src/Select/utils/commonStyleHooks.ts new file mode 100644 index 000000000..5d51afa7f --- /dev/null +++ b/packages/mui-base/src/Select/utils/commonStyleHooks.ts @@ -0,0 +1,5 @@ +export const commonStyleHooks = { + open: (value: boolean) => ({ + 'data-select': value ? 'open' : 'closed', + }), +}; From 7895e31bae33e08bdd4af51fbd65bfaeeffe8595 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 15 Aug 2024 15:13:53 +1000 Subject: [PATCH 02/94] Add selection logic --- .../api-docs/select-root/select-root.json | 4 +- .../mui-base/src/Select/Item/SelectItem.tsx | 72 +++++++++++---- .../mui-base/src/Select/Item/useSelectItem.ts | 56 +++++++----- .../Select/Positioner/SelectPositioner.tsx | 6 +- .../mui-base/src/Select/Root/SelectRoot.tsx | 28 +----- .../src/Select/Root/SelectRootContext.ts | 1 + .../src/Select/Root/useSelectRoot.tsx | 87 ++++++++++++------- .../src/Select/Trigger/SelectTrigger.tsx | 25 ++---- .../src/Select/Trigger/useSelectTrigger.ts | 68 +++++---------- 9 files changed, 180 insertions(+), 167 deletions(-) diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json index bcc4b0296..aeb69c07d 100644 --- a/docs/translations/api-docs/select-root/select-root.json +++ b/docs/translations/api-docs/select-root/select-root.json @@ -5,7 +5,6 @@ "description": "If true, the Menu supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." }, "defaultOpen": { "description": "If true, the Menu is initially open." }, - "dir": { "description": "The direction of the Menu (left-to-right or right-to-left)." }, "disabled": { "description": "If true, the Menu is disabled." }, "loop": { "description": "If true, using keyboard navigation will wrap focus to the other end of the list once the end is reached." @@ -15,8 +14,7 @@ }, "open": { "description": "Allows to control whether the dropdown is open. This is a controlled counterpart of defaultOpen." - }, - "orientation": { "description": "The orientation of the Menu (horizontal or vertical)." } + } }, "classDescriptions": {} } diff --git a/packages/mui-base/src/Select/Item/SelectItem.tsx b/packages/mui-base/src/Select/Item/SelectItem.tsx index 96f67b393..9bc10e4b5 100644 --- a/packages/mui-base/src/Select/Item/SelectItem.tsx +++ b/packages/mui-base/src/Select/Item/SelectItem.tsx @@ -1,13 +1,14 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { useListItem } from '@floating-ui/react'; +import { UseInteractionsReturn, useListItem } from '@floating-ui/react'; import { useSelectItem } from './useSelectItem'; -import { useSelectRootContext } from '../Root/SelectRootContext'; +import { SelectRootContext, useSelectRootContext } from '../Root/SelectRootContext'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useId } from '../../utils/useId'; -import type { BaseUIComponentProps, GenericHTMLProps } from '../../utils/types'; +import type { BaseUIComponentProps } from '../../utils/types'; import { useForkRef } from '../../utils/useForkRef'; +import { useEventCallback } from '../../utils/useEventCallback'; const InnerSelectItem = React.memo( React.forwardRef(function InnerSelectItem( @@ -19,32 +20,47 @@ const InnerSelectItem = React.memo( closeOnClick = true, disabled = false, highlighted, + selected, id, - propGetter, + getItemProps: getRootItemProps, render, treatMouseupAsClick, + setOpen, + typingRef, + handleSelect, ...otherProps } = props; - const { getRootProps } = useSelectItem({ + const { getItemProps } = useSelectItem({ + setOpen, closeOnClick, disabled, highlighted, id, ref: forwardedRef, treatMouseupAsClick, + typingRef, + handleSelect, }); const ownerState: SelectItem.OwnerState = React.useMemo( - () => ({ disabled, highlighted }), - [disabled, highlighted], + () => ({ disabled, highlighted, selected }), + [disabled, highlighted, selected], ); const { renderElement } = useComponentRenderer({ render: render ?? 'div', className, ownerState, - propGetter: (externalProps) => propGetter(getRootProps(externalProps)), + propGetter: (externalProps) => { + // Preserve the component prop `id` if it's provided. + const { id: idProp, ...rootItemProps } = getRootItemProps({ + ...externalProps, + selected, + active: highlighted, + }); + return getItemProps(rootItemProps); + }, extraProps: otherProps, }); @@ -67,17 +83,30 @@ const SelectItem = React.forwardRef(function SelectItem( props: SelectItem.Props, forwardedRef: React.ForwardedRef, ) { - const { id: idProp, label, ...other } = props; - - const { getItemProps, activeIndex, clickAndDragEnabled } = useSelectRootContext(); + const { id: idProp, label, ...otherProps } = props; + + const { + getItemProps, + activeIndex, + selectedIndex, + clickAndDragEnabled, + setOpen, + typingRef, + setSelectedIndex, + } = useSelectRootContext(); const itemRef = React.useRef(null); - const listItem = useListItem({ label: label ?? itemRef.current?.innerText }); + const listItem = useListItem({ label: label ?? itemRef.current?.textContent }); const mergedRef = useForkRef(forwardedRef, listItem.ref, itemRef); const id = useId(idProp); const highlighted = listItem.index === activeIndex; + const selected = listItem.index === selectedIndex; + + const handleSelect = useEventCallback(() => { + setSelectedIndex(listItem.index); + }); // This wrapper component is used as a performance optimization. // SelectItem reads the context and re-renders the actual SelectItem @@ -85,27 +114,36 @@ const SelectItem = React.forwardRef(function SelectItem( return ( ); }); interface InnerSelectItemProps extends SelectItem.Props { highlighted: boolean; - propGetter: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + selected: boolean; + getItemProps: UseInteractionsReturn['getItemProps']; treatMouseupAsClick: boolean; + setOpen: SelectRootContext['setOpen']; + typingRef: React.MutableRefObject; + handleSelect: () => void; } namespace SelectItem { - export type OwnerState = { + export interface OwnerState { disabled: boolean; highlighted: boolean; - }; + selected: boolean; + } export interface Props extends BaseUIComponentProps<'div', OwnerState> { children?: React.ReactNode; diff --git a/packages/mui-base/src/Select/Item/useSelectItem.ts b/packages/mui-base/src/Select/Item/useSelectItem.ts index 312f88158..3aa8d04a5 100644 --- a/packages/mui-base/src/Select/Item/useSelectItem.ts +++ b/packages/mui-base/src/Select/Item/useSelectItem.ts @@ -3,6 +3,7 @@ import * as React from 'react'; import type { GenericHTMLProps } from '../../utils/types'; import { useButton } from '../../useButton'; import { mergeReactProps } from '../../utils/mergeReactProps'; +import { SelectRootContext } from '../Root/SelectRootContext'; /** * @@ -11,7 +12,16 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; * - [useSelectItem API](https://mui.com/base-ui/api/use-select-item/) */ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.ReturnValue { - const { disabled = false, highlighted, id, ref: externalRef, treatMouseupAsClick } = params; + const { + disabled = false, + highlighted, + id, + ref: externalRef, + treatMouseupAsClick, + setOpen, + typingRef, + handleSelect, + } = params; const { getRootProps: getButtonProps, rootRef: mergedRef } = useButton({ disabled, @@ -19,45 +29,52 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R rootRef: externalRef, }); - const getRootProps = React.useCallback( + const getItemProps = React.useCallback( (externalProps?: GenericHTMLProps): GenericHTMLProps => { return getButtonProps( - mergeReactProps(externalProps, { - 'data-handle-mouseup': treatMouseupAsClick || undefined, + mergeReactProps<'div'>(externalProps, { + ['data-handle-mouseup' as string]: treatMouseupAsClick || undefined, id, - role: 'SelectItem', tabIndex: highlighted ? 0 : -1, + onClick(event) { + if (typingRef.current) { + return; + } + + handleSelect(); + setOpen(false, event.nativeEvent); + }, }), ); }, - [getButtonProps, highlighted, id, treatMouseupAsClick], + [getButtonProps, handleSelect, highlighted, id, setOpen, treatMouseupAsClick, typingRef], ); return React.useMemo( () => ({ - getRootProps, + getItemProps, rootRef: mergedRef, }), - [getRootProps, mergedRef], + [getItemProps, mergedRef], ); } export namespace useSelectItem { export interface Parameters { /** - * If `true`, the menu will close when the menu item is clicked. + * If `true`, the select will close when the select item is clicked. */ closeOnClick: boolean; /** - * If `true`, the menu item will be disabled. + * If `true`, the select item will be disabled. */ disabled: boolean; /** - * Determines if the menu item is highlighted. + * Determines if the select item is highlighted. */ highlighted: boolean; /** - * The id of the menu item. + * The id of the select item. */ id: string | undefined; /** @@ -65,21 +82,16 @@ export namespace useSelectItem { */ ref?: React.Ref; /** - * If `true`, the menu item will listen for mouseup events and treat them as clicks. + * If `true`, the select item will listen for mouseup events and treat them as clicks. */ treatMouseupAsClick: boolean; + setOpen: SelectRootContext['setOpen']; + typingRef: React.MutableRefObject; + handleSelect: () => void; } export interface ReturnValue { - /** - * Resolver for the root slot's props. - * @param externalProps event handlers for the root slot - * @returns props that should be spread on the root slot - */ - getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; - /** - * The ref to the component's root DOM element. - */ + getItemProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; rootRef: React.RefCallback | null; } } diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 73dee4853..e45c43472 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -57,8 +57,8 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( floatingRootContext, getPositionerProps, setPositionerElement, - itemDomElements, - itemLabels, + elementsRef, + labelsRef, triggerElement, mounted, } = useSelectRootContext(); @@ -133,7 +133,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( return ( - + >; + typingRef: React.MutableRefObject; } export const SelectRootContext = React.createContext(null); diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index ac7441044..ecd12b000 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -5,6 +5,7 @@ import { useDismiss, useFloatingRootContext, useInteractions, + UseInteractionsReturn, useListNavigation, useRole, useTypeahead, @@ -30,24 +31,44 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R open: openParam, defaultOpen, onOpenChange, - orientation, disabled, loop, + value, + onValueChange, + defaultValue, } = params; const [triggerElement, setTriggerElement] = React.useState(null); const [positionerElement, setPositionerElement] = React.useState(null); - const popupRef = React.useRef(null); const [activeIndex, setActiveIndex] = React.useState(null); - const [selectedIndex, setSelectedIndex] = React.useState(null); + const [selectedIndex, setSelectedIndexUnwrapped] = React.useState(null); + + const popupRef = React.useRef(null); + const typingRef = React.useRef(false); + const elementsRef = React.useRef>([]); + const labelsRef = React.useRef>([]); const [open, setOpenUnwrapped] = useControlled({ controlled: openParam, default: defaultOpen, - name: 'useSelectRoot', + name: 'Select', state: 'open', }); + const [selectedValue, setSelectedValueUnwrapped] = useControlled({ + controlled: value, + default: defaultValue, + name: 'Select', + state: 'selectedValue', + }); + + const setSelectedIndex = useEventCallback((index: number | null) => { + const nextValue = index === null ? '' : labelsRef.current[index] || ''; + setSelectedIndexUnwrapped(index); + setSelectedValueUnwrapped(nextValue); + onValueChange?.(nextValue); + }); + const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, animated); const runOnceAnimationsFinish = useAnimationsFinished(popupRef); @@ -77,31 +98,32 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R event: 'mousedown', }); - const dismiss = useDismiss(floatingRootContext); + const dismiss = useDismiss(floatingRootContext, { + enabled: !disabled, + }); const role = useRole(floatingRootContext, { role: 'select', }); - const itemDomElements = React.useRef<(HTMLElement | null)[]>([]); - const itemLabels = React.useRef<(string | null)[]>([]); - const listNavigation = useListNavigation(floatingRootContext, { enabled: !disabled, - listRef: itemDomElements, + listRef: elementsRef, + disabledIndices: EMPTY_ARRAY, activeIndex, selectedIndex, loop, - orientation, - disabledIndices: EMPTY_ARRAY, onNavigate: setActiveIndex, }); const typeahead = useTypeahead(floatingRootContext, { - listRef: itemLabels, + listRef: labelsRef, activeIndex, resetMs: 350, - onMatch: open ? setActiveIndex : undefined, + onMatch: open ? setActiveIndex : setSelectedIndex, + onTypingChange(typing) { + typingRef.current = typing; + }, }); const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ @@ -128,6 +150,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setActiveIndex, selectedIndex, setSelectedIndex, + selectedValue, floatingRootContext, triggerElement, setTriggerElement, @@ -135,24 +158,25 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setPositionerElement, getPositionerProps, getItemProps, - itemDomElements, - itemLabels, + elementsRef, + labelsRef, mounted, transitionStatus, popupRef, open, setOpen, + typingRef, }), [ activeIndex, selectedIndex, + setSelectedIndex, + selectedValue, floatingRootContext, triggerElement, getTriggerProps, getPositionerProps, getItemProps, - itemDomElements, - itemLabels, mounted, transitionStatus, open, @@ -161,19 +185,15 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R ); } -export type MenuOrientation = 'horizontal' | 'vertical'; - -export type MenuDirection = 'ltr' | 'rtl'; - export namespace useSelectRoot { export interface Parameters { /** - * If `true`, the Menu supports CSS-based animations and transitions. + * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. */ animated: boolean; /** - * Allows to control whether the Menu is open. + * Allows to control whether the Select is open. * This is a controlled counterpart of `defaultOpen`. */ open: boolean | undefined; @@ -182,7 +202,7 @@ export namespace useSelectRoot { */ onOpenChange: ((open: boolean, event: Event | undefined) => void) | undefined; /** - * If `true`, the Menu is initially open. + * If `true`, the Select is initially open. */ defaultOpen: boolean; /** @@ -190,26 +210,26 @@ export namespace useSelectRoot { */ loop: boolean; /** - * The orientation of the Menu (horizontal or vertical). - */ - orientation: MenuOrientation; - /** - * If `true`, the Menu is disabled. + * If `true`, the Select is disabled. */ disabled: boolean; + value?: string; + onValueChange?: (value: string) => void; + defaultValue?: string; } export interface ReturnValue { activeIndex: number | null; setActiveIndex: React.Dispatch>; selectedIndex: number | null; - setSelectedIndex: React.Dispatch>; + setSelectedIndex: (index: number | null) => void; + selectedValue: string | null; floatingRootContext: FloatingRootContext; - getItemProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + getItemProps: UseInteractionsReturn['getItemProps']; getPositionerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; - itemDomElements: React.MutableRefObject<(HTMLElement | null)[]>; - itemLabels: React.MutableRefObject<(string | null)[]>; + elementsRef: React.MutableRefObject<(HTMLElement | null)[]>; + labelsRef: React.MutableRefObject<(string | null)[]>; mounted: boolean; open: boolean; popupRef: React.RefObject; @@ -218,5 +238,6 @@ export namespace useSelectRoot { setTriggerElement: (element: HTMLElement | null) => void; transitionStatus: 'entering' | 'exiting' | undefined; triggerElement: HTMLElement | null; + typingRef: React.MutableRefObject; } } diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx index 53a71ecf7..b0fa40472 100644 --- a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx +++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx @@ -11,24 +11,17 @@ const SelectTrigger = React.forwardRef(function SelectTrigger( props: SelectTrigger.Props, forwardedRef: React.ForwardedRef, ) { - const { render, className, disabled = false, label, ...other } = props; + const { render, className, disabled = false, label, ...otherProps } = props; const { - getTriggerProps, - disabled: menuDisabled, - setTriggerElement, + getTriggerProps: getRootTriggerProps, + disabled: selectDisabled, open, - setOpen, - setClickAndDragEnabled, } = useSelectRootContext(); - const { getRootProps } = useSelectTrigger({ - disabled: disabled || menuDisabled, + const { getTriggerProps } = useSelectTrigger({ + disabled: disabled || selectDisabled, rootRef: forwardedRef, - setTriggerElement, - open, - setOpen, - setClickAndDragEnabled, }); const ownerState: SelectTrigger.OwnerState = React.useMemo(() => ({ open }), [open]); @@ -37,9 +30,9 @@ const SelectTrigger = React.forwardRef(function SelectTrigger( render: render ?? 'button', className, ownerState, - propGetter: (externalProps) => getTriggerProps(getRootProps(externalProps)), + propGetter: (externalProps) => getRootTriggerProps(getTriggerProps(externalProps)), customStyleHookMapping: commonStyleHooks, - extraProps: other, + extraProps: otherProps, }); return renderElement(); @@ -64,9 +57,9 @@ namespace SelectTrigger { label?: string; } - export type OwnerState = { + export interface OwnerState { open: boolean; - }; + } } SelectTrigger.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts index 03ed6bf73..0fd46a7dc 100644 --- a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts +++ b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts @@ -1,10 +1,12 @@ 'use client'; import * as React from 'react'; +import { getTarget } from '@floating-ui/react/utils'; import { useButton } from '../../useButton/useButton'; import type { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { ownerDocument } from '../../utils/owner'; import { useForkRef } from '../../utils/useForkRef'; +import { useSelectRootContext } from '../Root/SelectRootContext'; /** * @@ -15,20 +17,16 @@ import { useForkRef } from '../../utils/useForkRef'; export function useSelectTrigger( parameters: useSelectTrigger.Parameters, ): useSelectTrigger.ReturnValue { - const { - disabled = false, - rootRef: externalRef, - open, - setOpen, - setTriggerElement, - setClickAndDragEnabled, - } = parameters; + const { disabled = false, rootRef: externalRef } = parameters; + + const { selectedValue, open, setOpen, setClickAndDragEnabled, setTriggerElement } = + useSelectRootContext(); const triggerRef = React.useRef(null); const mergedRef = useForkRef(externalRef, triggerRef); - const { getRootProps: getButtonRootProps, rootRef: buttonRootRef } = useButton({ + const { getRootProps: getButtonProps, rootRef: buttonRootRef } = useButton({ disabled, focusableWhenDisabled: false, rootRef: mergedRef, @@ -37,14 +35,14 @@ export function useSelectTrigger( const handleRef = useForkRef(buttonRootRef, setTriggerElement); const ignoreNextClickRef = React.useRef(false); - const getRootProps = React.useCallback( + const getTriggerProps = React.useCallback( (externalProps?: GenericHTMLProps): GenericHTMLProps => { - return mergeReactProps( - externalProps, + return mergeReactProps<'div'>( + { ...externalProps, children: selectedValue ?? externalProps?.children }, { tabIndex: 0, // this is needed to make the button focused after click in Safari ref: handleRef, - onMouseDown: (event: MouseEvent) => { + onMouseDown(event) { if (open) { return; } @@ -54,7 +52,9 @@ export function useSelectTrigger( event.preventDefault(); setClickAndDragEnabled(true); - const mousedownTarget = event.target as Element; + + const mousedownTarget = getTarget(event.nativeEvent) as Element | null; + const doc = ownerDocument(mousedownTarget); function handleDocumentMouseUp(mouseUpEvent: MouseEvent) { const mouseupTarget = mouseUpEvent.target as HTMLElement; @@ -68,29 +68,29 @@ export function useSelectTrigger( } setClickAndDragEnabled(false); - ownerDocument(mousedownTarget).removeEventListener('mouseup', handleDocumentMouseUp); + doc.removeEventListener('mouseup', handleDocumentMouseUp); } - ownerDocument(mousedownTarget).addEventListener('mouseup', handleDocumentMouseUp); + doc.addEventListener('mouseup', handleDocumentMouseUp); }, - onClick: () => { + onClick() { if (ignoreNextClickRef.current) { ignoreNextClickRef.current = false; } }, }, - getButtonRootProps(), + getButtonProps(), ); }, - [getButtonRootProps, handleRef, open, setOpen, setClickAndDragEnabled], + [selectedValue, handleRef, getButtonProps, open, setClickAndDragEnabled, setOpen], ); return React.useMemo( () => ({ - getRootProps, + getTriggerProps, rootRef: handleRef, }), - [getRootProps, handleRef], + [getTriggerProps, handleRef], ); } @@ -105,34 +105,10 @@ export namespace useSelectTrigger { * The ref to the root element. */ rootRef?: React.Ref; - /** - * A callback to set the trigger element whenever it's mounted. - */ - setTriggerElement: (element: HTMLElement | null) => void; - /** - * If `true`, the Menu is open. - */ - open: boolean; - /** - * A callback to set the open state of the Menu. - */ - setOpen: (open: boolean, event: Event | undefined) => void; - /** - * A callback to enable/disable click and drag functionality. - */ - setClickAndDragEnabled: (enabled: boolean) => void; } export interface ReturnValue { - /** - * Resolver for the root slot's props. - * @param externalProps props for the root slot - * @returns props that should be spread on the root slot - */ - getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; - /** - * The ref to the root element. - */ + getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; rootRef: React.RefCallback | null; } } From 205882c6dc2c187b47a6995076e3eb24bf6ecbf9 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 15 Aug 2024 15:18:42 +1000 Subject: [PATCH 03/94] Remove a11y route --- docs/data/base/pages.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts index dff263ebb..758d6f08a 100644 --- a/docs/data/base/pages.ts +++ b/docs/data/base/pages.ts @@ -8,7 +8,6 @@ const pages: readonly MuiPage[] = [ { pathname: '/base-ui/getting-started', title: 'Overview' }, { pathname: '/base-ui/getting-started/quickstart', title: 'Quickstart' }, { pathname: '/base-ui/getting-started/usage', title: 'Usage' }, - { pathname: '/base-ui/getting-started/accessibility', title: 'Accessibility' }, { pathname: '/base-ui/getting-started/support' }, ], }, From af579634c85a97da4ccdc882c9d9de20d152f04d Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 16 Aug 2024 14:42:55 +1000 Subject: [PATCH 04/94] Add inner anchoring --- docs/data/api/use-select-popup.json | 8 ++ .../select/SelectIntroduction/system/index.js | 73 ++++++++++++-- .../SelectIntroduction/system/index.tsx | 73 ++++++++++++-- .../system/index.tsx.preview | 10 -- docs/pages/base-ui/api/select-positioner.json | 62 ++++++++++++ .../select-positioner/select-positioner.json | 27 +++--- .../use-select-popup/use-select-popup.json | 1 + docs/translations/translations.json | 1 - .../mui-base/src/Select/Item/SelectItem.tsx | 20 ++-- .../mui-base/src/Select/Item/useSelectItem.ts | 59 ++++++++++-- .../mui-base/src/Select/Popup/SelectPopup.tsx | 14 ++- .../src/Select/Popup/useSelectPopup.ts | 41 ++++++++ .../Select/Positioner/SelectPositioner.tsx | 95 ++++++++++++------- .../Select/Positioner/useSelectPositioner.tsx | 54 +++++++---- .../mui-base/src/Select/Root/SelectRoot.tsx | 6 +- .../src/Select/Root/SelectRootContext.ts | 2 - .../src/Select/Root/useSelectRoot.tsx | 54 +++++++++-- .../src/Select/Trigger/useSelectTrigger.ts | 60 ++++++------ .../src/utils/useAnchorPositioning.ts | 44 +++++---- 19 files changed, 523 insertions(+), 181 deletions(-) create mode 100644 docs/data/api/use-select-popup.json delete mode 100644 docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview create mode 100644 docs/pages/base-ui/api/select-positioner.json create mode 100644 docs/translations/api-docs/use-select-popup/use-select-popup.json create mode 100644 packages/mui-base/src/Select/Popup/useSelectPopup.ts diff --git a/docs/data/api/use-select-popup.json b/docs/data/api/use-select-popup.json new file mode 100644 index 000000000..e93f3c3c6 --- /dev/null +++ b/docs/data/api/use-select-popup.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useSelectPopup", + "filename": "/packages/mui-base/src/Select/Popup/useSelectPopup.ts", + "imports": ["import { useSelectPopup } from '@base_ui/react/Select';"], + "demos": "
    " +} diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 762ad7aca..6754e11c6 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -1,17 +1,76 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; +import { styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; export default function UnstyledSelectIntroduction() { return ( - Trigger - - - Item 1 - Item 2 - Item 3 - + Trigger + + + {[...Array(100)].map((_, index) => ( + ( +
    + Item {index + 1}{' '} + {state.selected && } +
    + )} + /> + ))} +
    ); } + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(0, 0, 0, 0.1); + max-height: var(--available-height); +`; + +const SelectItem = styled(Select.Item)` + padding: 6px 12px; + outline: 0; + cursor: default; + border-radius: 4px; + scroll-margin: 4px; + user-select: none; + display: flex; + align-items: center; + justify-content: space-between; + line-height: 1.5; + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectCheck = styled(Check)` + margin-left: 8px; +`; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 762ad7aca..6754e11c6 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -1,17 +1,76 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; +import { styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; export default function UnstyledSelectIntroduction() { return ( - Trigger - - - Item 1 - Item 2 - Item 3 - + Trigger + + + {[...Array(100)].map((_, index) => ( + ( +
    + Item {index + 1}{' '} + {state.selected && } +
    + )} + /> + ))} +
    ); } + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(0, 0, 0, 0.1); + max-height: var(--available-height); +`; + +const SelectItem = styled(Select.Item)` + padding: 6px 12px; + outline: 0; + cursor: default; + border-radius: 4px; + scroll-margin: 4px; + user-select: none; + display: flex; + align-items: center; + justify-content: space-between; + line-height: 1.5; + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectCheck = styled(Check)` + margin-left: 8px; +`; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview deleted file mode 100644 index db7e28b51..000000000 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview +++ /dev/null @@ -1,10 +0,0 @@ - - Trigger - - - Item 1 - Item 2 - Item 3 - - - \ No newline at end of file diff --git a/docs/pages/base-ui/api/select-positioner.json b/docs/pages/base-ui/api/select-positioner.json new file mode 100644 index 000000000..1937b2bb5 --- /dev/null +++ b/docs/pages/base-ui/api/select-positioner.json @@ -0,0 +1,62 @@ +{ + "props": { + "alignment": { + "type": { + "name": "enum", + "description": "'center'
    | 'end'
    | 'start'" + }, + "default": "'center'" + }, + "alignmentOffset": { "type": { "name": "number" }, "default": "0" }, + "anchor": { + "type": { + "name": "union", + "description": "HTML element
    | object
    | func" + } + }, + "anchorToItem": { "type": { "name": "bool" }, "default": "false" }, + "arrowPadding": { "type": { "name": "number" }, "default": "5" }, + "className": { "type": { "name": "union", "description": "func
    | string" } }, + "collisionBoundary": { + "type": { + "name": "union", + "description": "HTML element
    | Array<HTML element>
    | string
    | { height?: number, width?: number, x?: number, y?: number }" + }, + "default": "'clippingAncestors'" + }, + "collisionPadding": { + "type": { + "name": "union", + "description": "number
    | { bottom?: number, left?: number, right?: number, top?: number }" + }, + "default": "5" + }, + "container": { "type": { "name": "union", "description": "HTML element
    | func" } }, + "hideWhenDetached": { "type": { "name": "bool" }, "default": "false" }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "positionStrategy": { + "type": { "name": "enum", "description": "'absolute'
    | 'fixed'" }, + "default": "'absolute'" + }, + "render": { "type": { "name": "union", "description": "element
    | func" } }, + "side": { + "type": { + "name": "enum", + "description": "'bottom'
    | 'left'
    | 'right'
    | 'top'" + }, + "default": "'bottom'" + }, + "sideOffset": { "type": { "name": "number" }, "default": "0" }, + "sticky": { "type": { "name": "bool" }, "default": "false" } + }, + "name": "SelectPositioner", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectPositioner = Select.Positioner;" + ], + "classes": [], + "muiName": "SelectPositioner", + "filename": "/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/translations/api-docs/select-positioner/select-positioner.json b/docs/translations/api-docs/select-positioner/select-positioner.json index cfd19a4bc..80f0b1a36 100644 --- a/docs/translations/api-docs/select-positioner/select-positioner.json +++ b/docs/translations/api-docs/select-positioner/select-positioner.json @@ -2,41 +2,44 @@ "componentDescription": "Renders the element that positions the Select popup.", "propDescriptions": { "alignment": { - "description": "The alignment of the Menu element to the anchor element along its cross axis." + "description": "The alignment of the Select element to the anchor element along its cross axis." }, "alignmentOffset": { - "description": "The offset of the Menu element along its alignment axis." + "description": "The offset of the Select element along its alignment axis." + }, + "anchor": { "description": "The anchor element to which the Select popup will be placed at." }, + "anchorToItem": { + "description": "If true, positions the popup relative to the selected item inside it." }, - "anchor": { "description": "The anchor element to which the Menu popup will be placed at." }, "arrowPadding": { - "description": "Determines the padding between the arrow and the Menu popup's edges. Useful when the popover popup has rounded corners via border-radius." + "description": "Determines the padding between the arrow and the Select popup's edges. Useful when the popover popup has rounded corners via border-radius." }, "className": { "description": "Class names applied to the element or a function that returns them based on the component's state." }, "collisionBoundary": { - "description": "The boundary that the Menu element should be constrained to." + "description": "The boundary that the Select element should be constrained to." }, "collisionPadding": { "description": "The padding of the collision boundary." }, "container": { - "description": "The container element to which the Menu popup will be appended to." + "description": "The container element to which the Select popup will be appended to." }, "hideWhenDetached": { - "description": "If true, the Menu will be hidden if it is detached from its anchor element due to differing clipping contexts." + "description": "If true, the Select will be hidden if it is detached from its anchor element due to differing clipping contexts." }, "keepMounted": { - "description": "Whether the menu popup remains mounted in the DOM while closed." + "description": "Whether the select popup remains mounted in the DOM while closed." }, "positionStrategy": { - "description": "The CSS position strategy for positioning the Menu popup element." + "description": "The CSS position strategy for positioning the Select popup element." }, "render": { "description": "A function to customize rendering of the component." }, "side": { - "description": "The side of the anchor element that the Menu element should align to." + "description": "The side of the anchor element that the Select element should align to." }, - "sideOffset": { "description": "The gap between the anchor element and the Menu element." }, + "sideOffset": { "description": "The gap between the anchor element and the Select element." }, "sticky": { - "description": "If true, allow the Menu to remain in stuck view while the anchor element is scrolled out of view." + "description": "If true, allow the Select to remain in stuck view while the anchor element is scrolled out of view." } }, "classDescriptions": {} diff --git a/docs/translations/api-docs/use-select-popup/use-select-popup.json b/docs/translations/api-docs/use-select-popup/use-select-popup.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-select-popup/use-select-popup.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 4058a0243..b38a689c7 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -215,7 +215,6 @@ "/base-ui/getting-started": "Overview", "/base-ui/getting-started/quickstart": "Quickstart", "/base-ui/getting-started/usage": "Usage", - "/base-ui/getting-started/accessibility": "Accessibility", "/base-ui/getting-started/support": "Support", "/base-ui/react-": "Components", "/base-ui/all-components": "All components", diff --git a/packages/mui-base/src/Select/Item/SelectItem.tsx b/packages/mui-base/src/Select/Item/SelectItem.tsx index 9bc10e4b5..7f90a115c 100644 --- a/packages/mui-base/src/Select/Item/SelectItem.tsx +++ b/packages/mui-base/src/Select/Item/SelectItem.tsx @@ -24,10 +24,10 @@ const InnerSelectItem = React.memo( id, getItemProps: getRootItemProps, render, - treatMouseupAsClick, setOpen, typingRef, handleSelect, + selectionRef, ...otherProps } = props; @@ -36,11 +36,12 @@ const InnerSelectItem = React.memo( closeOnClick, disabled, highlighted, + selected, id, ref: forwardedRef, - treatMouseupAsClick, typingRef, handleSelect, + selectionRef, }); const ownerState: SelectItem.OwnerState = React.useMemo( @@ -89,15 +90,15 @@ const SelectItem = React.forwardRef(function SelectItem( getItemProps, activeIndex, selectedIndex, - clickAndDragEnabled, setOpen, typingRef, setSelectedIndex, + selectionRef, } = useSelectRootContext(); - const itemRef = React.useRef(null); - const listItem = useListItem({ label: label ?? itemRef.current?.textContent }); - const mergedRef = useForkRef(forwardedRef, listItem.ref, itemRef); + const [item, setItem] = React.useState(null); + const listItem = useListItem({ label: label ?? item?.textContent }); + const mergedRef = useForkRef(forwardedRef, listItem.ref, setItem); const id = useId(idProp); @@ -119,10 +120,10 @@ const SelectItem = React.forwardRef(function SelectItem( ref={mergedRef} highlighted={highlighted} handleSelect={handleSelect} + selectionRef={selectionRef} setOpen={setOpen} selected={selected} getItemProps={getItemProps} - treatMouseupAsClick={clickAndDragEnabled} typingRef={typingRef} /> ); @@ -132,10 +133,13 @@ interface InnerSelectItemProps extends SelectItem.Props { highlighted: boolean; selected: boolean; getItemProps: UseInteractionsReturn['getItemProps']; - treatMouseupAsClick: boolean; setOpen: SelectRootContext['setOpen']; typingRef: React.MutableRefObject; handleSelect: () => void; + selectionRef: React.MutableRefObject<{ + mouseUp: boolean; + select: boolean; + }>; } namespace SelectItem { diff --git a/packages/mui-base/src/Select/Item/useSelectItem.ts b/packages/mui-base/src/Select/Item/useSelectItem.ts index 3aa8d04a5..08c890006 100644 --- a/packages/mui-base/src/Select/Item/useSelectItem.ts +++ b/packages/mui-base/src/Select/Item/useSelectItem.ts @@ -4,6 +4,7 @@ import type { GenericHTMLProps } from '../../utils/types'; import { useButton } from '../../useButton'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { SelectRootContext } from '../Root/SelectRootContext'; +import { useEventCallback } from '../../utils/useEventCallback'; /** * @@ -15,12 +16,13 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R const { disabled = false, highlighted, + selected, id, ref: externalRef, - treatMouseupAsClick, setOpen, typingRef, handleSelect, + selectionRef, } = params; const { getRootProps: getButtonProps, rootRef: mergedRef } = useButton({ @@ -29,25 +31,58 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R rootRef: externalRef, }); + const commitSelection = useEventCallback((event: Event) => { + handleSelect(); + setOpen(false, event); + }); + + const lastKeyRef = React.useRef(null); + const getItemProps = React.useCallback( (externalProps?: GenericHTMLProps): GenericHTMLProps => { return getButtonProps( mergeReactProps<'div'>(externalProps, { - ['data-handle-mouseup' as string]: treatMouseupAsClick || undefined, id, tabIndex: highlighted ? 0 : -1, + onTouchStart() { + selectionRef.current = { + mouseUp: false, + select: true, + }; + }, + onKeyDown(event) { + selectionRef.current.select = true; + lastKeyRef.current = event.key; + }, onClick(event) { - if (typingRef.current) { + if (lastKeyRef.current === ' ' && typingRef.current) { + return; + } + + if (selectionRef.current.select) { + lastKeyRef.current = null; + commitSelection(event.nativeEvent); + } + }, + onMouseUp(event) { + if (!selectionRef.current.mouseUp) { return; } - handleSelect(); - setOpen(false, event.nativeEvent); + if (selected) { + if (selectionRef.current.select) { + commitSelection(event.nativeEvent); + } + } else { + commitSelection(event.nativeEvent); + } + + selectionRef.current.select = true; }, }), ); }, - [getButtonProps, handleSelect, highlighted, id, setOpen, treatMouseupAsClick, typingRef], + [commitSelection, getButtonProps, highlighted, id, selected, selectionRef, typingRef], ); return React.useMemo( @@ -73,6 +108,10 @@ export namespace useSelectItem { * Determines if the select item is highlighted. */ highlighted: boolean; + /** + * Determines if the select item is selected. + */ + selected: boolean; /** * The id of the select item. */ @@ -81,13 +120,13 @@ export namespace useSelectItem { * The ref of the trigger element. */ ref?: React.Ref; - /** - * If `true`, the select item will listen for mouseup events and treat them as clicks. - */ - treatMouseupAsClick: boolean; setOpen: SelectRootContext['setOpen']; typingRef: React.MutableRefObject; handleSelect: () => void; + selectionRef: React.MutableRefObject<{ + mouseUp: boolean; + select: boolean; + }>; } export interface ReturnValue { diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.tsx index 132c641da..cf9b6ec64 100644 --- a/packages/mui-base/src/Select/Popup/SelectPopup.tsx +++ b/packages/mui-base/src/Select/Popup/SelectPopup.tsx @@ -9,14 +9,15 @@ import { commonStyleHooks } from '../utils/commonStyleHooks'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useForkRef } from '../../utils/useForkRef'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import { useSelectPopup } from './useSelectPopup'; const customStyleHookMapping: CustomStyleHookMapping = { ...commonStyleHooks, entering(value) { - return value ? { 'data-menu-entering': '' } : null; + return value ? { 'data-select-entering': '' } : null; }, exiting(value) { - return value ? { 'data-menu-exiting': '' } : null; + return value ? { 'data-select-exiting': '' } : null; }, }; @@ -24,10 +25,12 @@ const SelectPopup = React.forwardRef(function SelectPopup( props: SelectPopup.Props, forwardedRef: React.ForwardedRef, ) { - const { render, className, ...other } = props; + const { render, className, ...otherProps } = props; const { open, popupRef, transitionStatus } = useSelectRootContext(); const { side, alignment } = useSelectPositionerContext(); + const { getPopupProps } = useSelectPopup(); + const mergedRef = useForkRef(forwardedRef, popupRef); const ownerState: SelectPopup.OwnerState = React.useMemo( @@ -42,12 +45,13 @@ const SelectPopup = React.forwardRef(function SelectPopup( ); const { renderElement } = useComponentRenderer({ + propGetter: getPopupProps, render: render ?? 'div', + ref: mergedRef, className, ownerState, - extraProps: other, customStyleHookMapping, - ref: mergedRef, + extraProps: otherProps, }); return renderElement(); diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts new file mode 100644 index 000000000..779ac9bb1 --- /dev/null +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -0,0 +1,41 @@ +import * as React from 'react'; +import type { GenericHTMLProps } from '../../utils/types'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useSelectRootContext } from '../Root/SelectRootContext'; + +/** + * + * API: + * + * - [useSelectPopup API](https://mui.com/base-ui/api/use-select-popup/) + */ +export function useSelectPopup(): useSelectPopup.ReturnValue { + const { getPopupProps: getRootPopupProps } = useSelectRootContext(); + + const getPopupProps: useSelectPopup.ReturnValue['getPopupProps'] = React.useCallback( + (externalProps = {}) => { + return mergeReactProps<'div'>(getRootPopupProps(externalProps), { + style: { + overflowY: 'auto', + scrollbarWidth: 'none', + // must be relative to the element. + position: 'relative', + }, + }); + }, + [getRootPopupProps], + ); + + return React.useMemo( + () => ({ + getPopupProps, + }), + [getPopupProps], + ); +} + +namespace useSelectPopup { + export interface ReturnValue { + getPopupProps: (props?: GenericHTMLProps) => GenericHTMLProps; + } +} diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index e45c43472..8df0d5c56 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -4,11 +4,11 @@ import PropTypes from 'prop-types'; import { FloatingFocusManager, FloatingList, - FloatingNode, FloatingPortal, - Side, - useFloatingNodeId, + inner, + type Side, } from '@floating-ui/react'; +import type { BaseUIComponentProps } from '../../utils/types'; import { SelectPositionerContext } from './SelectPositionerContext'; import { useSelectRootContext } from '../Root/SelectRootContext'; import { commonStyleHooks } from '../utils/commonStyleHooks'; @@ -16,7 +16,6 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useForkRef } from '../../utils/useForkRef'; import { useSelectPositioner } from './useSelectPositioner'; import { HTMLElementType } from '../../utils/proptypes'; -import { BaseUIComponentProps, GenericHTMLProps } from '../../utils/types'; /** * Renders the element that positions the Select popup. @@ -35,6 +34,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( ) { const { anchor, + anchorToItem = false, positionStrategy = 'absolute', className, render, @@ -55,16 +55,17 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const { open, floatingRootContext, - getPositionerProps, setPositionerElement, elementsRef, labelsRef, triggerElement, mounted, + selectedIndex, + popupRef, + overflowRef, + innerOffset, } = useSelectRootContext(); - const nodeId = useFloatingNodeId(); - const positioner = useSelectPositioner({ anchor: anchor || triggerElement, floatingRootContext, @@ -81,7 +82,21 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( collisionPadding, hideWhenDetached, sticky, - nodeId, + allowAxisFlip: false, + inner: + anchorToItem && selectedIndex !== null + ? // Dependency-injected for tree-shaking purposes. Other floating element components don't + // use or need this. + inner({ + boundary: collisionBoundary, + padding: collisionPadding, + listRef: elementsRef, + index: selectedIndex, + scrollRef: popupRef, + offset: innerOffset, + overflowRef, + }) + : undefined, }); const ownerState: SelectPositioner.OwnerState = React.useMemo( @@ -115,8 +130,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const mergedRef = useForkRef(forwardedRef, setPositionerElement); const { renderElement } = useComponentRenderer({ - propGetter: (externalProps: GenericHTMLProps) => - positioner.getPositionerProps(getPositionerProps(externalProps)), + propGetter: positioner.getPositionerProps, render: render ?? 'div', className, ownerState, @@ -132,19 +146,17 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( return ( - - - - - {renderElement()} - - - - + + + + {renderElement()} + + + ); }); @@ -160,7 +172,13 @@ export namespace SelectPositioner { export interface Props extends useSelectPositioner.SharedParameters, - BaseUIComponentProps<'div', OwnerState> {} + BaseUIComponentProps<'div', OwnerState> { + /** + * If `true`, positions the popup relative to the selected item inside it. + * @default false + */ + anchorToItem?: boolean; + } } SelectPositioner.propTypes /* remove-proptypes */ = { @@ -169,17 +187,17 @@ SelectPositioner.propTypes /* remove-proptypes */ = { // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** - * The alignment of the Menu element to the anchor element along its cross axis. + * The alignment of the Select element to the anchor element along its cross axis. * @default 'center' */ alignment: PropTypes.oneOf(['center', 'end', 'start']), /** - * The offset of the Menu element along its alignment axis. + * The offset of the Select element along its alignment axis. * @default 0 */ alignmentOffset: PropTypes.number, /** - * The anchor element to which the Menu popup will be placed at. + * The anchor element to which the Select popup will be placed at. */ anchor: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ HTMLElementType, @@ -187,7 +205,12 @@ SelectPositioner.propTypes /* remove-proptypes */ = { PropTypes.func, ]), /** - * Determines the padding between the arrow and the Menu popup's edges. Useful when the popover + * If `true`, positions the popup relative to the selected item inside it. + * @default false + */ + anchorToItem: PropTypes.bool, + /** + * Determines the padding between the arrow and the Select popup's edges. Useful when the popover * popup has rounded corners via `border-radius`. * @default 5 */ @@ -201,7 +224,7 @@ SelectPositioner.propTypes /* remove-proptypes */ = { */ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** - * The boundary that the Menu element should be constrained to. + * The boundary that the Select element should be constrained to. * @default 'clippingAncestors' */ collisionBoundary: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ @@ -229,25 +252,25 @@ SelectPositioner.propTypes /* remove-proptypes */ = { }), ]), /** - * The container element to which the Menu popup will be appended to. + * The container element to which the Select popup will be appended to. */ container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ HTMLElementType, PropTypes.func, ]), /** - * If `true`, the Menu will be hidden if it is detached from its anchor element due to + * If `true`, the Select will be hidden if it is detached from its anchor element due to * differing clipping contexts. * @default false */ hideWhenDetached: PropTypes.bool, /** - * Whether the menu popup remains mounted in the DOM while closed. + * Whether the select popup remains mounted in the DOM while closed. * @default false */ keepMounted: PropTypes.bool, /** - * The CSS position strategy for positioning the Menu popup element. + * The CSS position strategy for positioning the Select popup element. * @default 'absolute' */ positionStrategy: PropTypes.oneOf(['absolute', 'fixed']), @@ -256,17 +279,17 @@ SelectPositioner.propTypes /* remove-proptypes */ = { */ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), /** - * The side of the anchor element that the Menu element should align to. + * The side of the anchor element that the Select element should align to. * @default 'bottom' */ side: PropTypes.oneOf(['bottom', 'left', 'right', 'top']), /** - * The gap between the anchor element and the Menu element. + * The gap between the anchor element and the Select element. * @default 0 */ sideOffset: PropTypes.number, /** - * If `true`, allow the Menu to remain in stuck view while the anchor element is scrolled out + * If `true`, allow the Select to remain in stuck view while the anchor element is scrolled out * of view. * @default false */ diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 0bcdeaa0d..486e71dd2 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -6,6 +6,7 @@ import type { Boundary, FloatingRootContext, FloatingContext, + Middleware, } from '@floating-ui/react'; import type { GenericHTMLProps } from '../../utils/types'; import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; @@ -42,12 +43,13 @@ export function useSelectPositioner( } return mergeReactProps(externalProps, { + tabIndex: -1, + inert: open ? undefined : 'true', style: { ...positionerStyles, ...hiddenStyles, zIndex: 2147483647, // max z-index }, - 'aria-hidden': !open || undefined, }); }, [positionerStyles, open, keepMounted, hidden], @@ -78,11 +80,11 @@ export function useSelectPositioner( export namespace useSelectPositioner { export interface SharedParameters { /** - * If `true`, the Menu is open. + * If `true`, the Select is open. */ open?: boolean; /** - * The anchor element to which the Menu popup will be placed at. + * The anchor element to which the Select popup will be placed at. */ anchor?: | Element @@ -91,36 +93,36 @@ export namespace useSelectPositioner { | React.MutableRefObject | (() => Element | VirtualElement | null); /** - * The CSS position strategy for positioning the Menu popup element. + * The CSS position strategy for positioning the Select popup element. * @default 'absolute' */ positionStrategy?: 'absolute' | 'fixed'; /** - * The container element to which the Menu popup will be appended to. + * The container element to which the Select popup will be appended to. */ container?: HTMLElement | null | React.MutableRefObject; /** - * The side of the anchor element that the Menu element should align to. + * The side of the anchor element that the Select element should align to. * @default 'bottom' */ side?: Side; /** - * The gap between the anchor element and the Menu element. + * The gap between the anchor element and the Select element. * @default 0 */ sideOffset?: number; /** - * The alignment of the Menu element to the anchor element along its cross axis. + * The alignment of the Select element to the anchor element along its cross axis. * @default 'center' */ alignment?: 'start' | 'end' | 'center'; /** - * The offset of the Menu element along its alignment axis. + * The offset of the Select element along its alignment axis. * @default 0 */ alignmentOffset?: number; /** - * The boundary that the Menu element should be constrained to. + * The boundary that the Select element should be constrained to. * @default 'clippingAncestors' */ collisionBoundary?: Boundary; @@ -130,24 +132,24 @@ export namespace useSelectPositioner { */ collisionPadding?: Padding; /** - * If `true`, the Menu will be hidden if it is detached from its anchor element due to + * If `true`, the Select will be hidden if it is detached from its anchor element due to * differing clipping contexts. * @default false */ hideWhenDetached?: boolean; /** - * Whether the menu popup remains mounted in the DOM while closed. + * Whether the select popup remains mounted in the DOM while closed. * @default false */ keepMounted?: boolean; /** - * If `true`, allow the Menu to remain in stuck view while the anchor element is scrolled out + * If `true`, allow the Select to remain in stuck view while the anchor element is scrolled out * of view. * @default false */ sticky?: boolean; /** - * Determines the padding between the arrow and the Menu popup's edges. Useful when the popover + * Determines the padding between the arrow and the Select popup's edges. Useful when the popover * popup has rounded corners via `border-radius`. * @default 5 */ @@ -156,27 +158,37 @@ export namespace useSelectPositioner { export interface Parameters extends SharedParameters { /** - * If `true`, the Menu is mounted. + * If `true`, the Select is mounted. * @default true */ mounted?: boolean; /** - * The Menu root context. + * The Select root context. */ floatingRootContext?: FloatingRootContext; /** * Floating node id. */ nodeId?: string; + /** + * If specified, positions the popup relative to the selected item inside it. + */ + inner?: Middleware; + /** + * Whether the floating element can flip to the perpendicular axis if it cannot fit in the + * viewport. + * @default true + */ + allowAxisFlip?: boolean; } export interface ReturnValue { /** - * Props to spread on the Menu positioner element. + * Props to spread on the Select positioner element. */ getPositionerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; /** - * The ref of the Menu arrow element. + * The ref of the Select arrow element. */ arrowRef: React.MutableRefObject; /** @@ -184,15 +196,15 @@ export namespace useSelectPositioner { */ arrowUncentered: boolean; /** - * The rendered side of the Menu element. + * The rendered side of the Select element. */ side: 'top' | 'right' | 'bottom' | 'left'; /** - * The rendered alignment of the Menu element. + * The rendered alignment of the Select element. */ alignment: 'start' | 'end' | 'center'; /** - * The styles to apply to the Menu arrow element. + * The styles to apply to the Select arrow element. */ arrowStyles: React.CSSProperties; /** diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index dcd6ca938..ecf3faefb 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -24,16 +24,12 @@ function SelectRoot(props: SelectRoot.Props) { open, }); - const [clickAndDragEnabled, setClickAndDragEnabled] = React.useState(false); - const context: SelectRootContext = React.useMemo( () => ({ ...selectRoot, disabled, - clickAndDragEnabled, - setClickAndDragEnabled, }), - [selectRoot, disabled, clickAndDragEnabled, setClickAndDragEnabled], + [selectRoot, disabled], ); return {children}; diff --git a/packages/mui-base/src/Select/Root/SelectRootContext.ts b/packages/mui-base/src/Select/Root/SelectRootContext.ts index 540df5560..91ad12d35 100644 --- a/packages/mui-base/src/Select/Root/SelectRootContext.ts +++ b/packages/mui-base/src/Select/Root/SelectRootContext.ts @@ -4,8 +4,6 @@ import type { useSelectRoot } from './useSelectRoot'; export interface SelectRootContext extends useSelectRoot.ReturnValue { disabled: boolean; - clickAndDragEnabled: boolean; - setClickAndDragEnabled: React.Dispatch>; typingRef: React.MutableRefObject; } diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index ecd12b000..a3174c27e 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -4,12 +4,14 @@ import { useClick, useDismiss, useFloatingRootContext, + useInnerOffset, useInteractions, UseInteractionsReturn, useListNavigation, useRole, useTypeahead, type FloatingRootContext, + type SideObject, } from '@floating-ui/react'; import type { GenericHTMLProps } from '../../utils/types'; import { useTransitionStatus } from '../../utils/useTransitionStatus'; @@ -42,11 +44,15 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const [positionerElement, setPositionerElement] = React.useState(null); const [activeIndex, setActiveIndex] = React.useState(null); const [selectedIndex, setSelectedIndexUnwrapped] = React.useState(null); + const [innerOffset, setInnerOffset] = React.useState(0); const popupRef = React.useRef(null); const typingRef = React.useRef(false); const elementsRef = React.useRef>([]); const labelsRef = React.useRef>([]); + const selectionRef = React.useRef({ mouseUp: false, select: false }); + const overflowRef = React.useRef({ top: 0, bottom: 0, left: 0, right: 0 }); + const selectedDelayedIndexRef = React.useRef(null); const [open, setOpenUnwrapped] = useControlled({ controlled: openParam, @@ -55,6 +61,10 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R state: 'open', }); + if (innerOffset !== 0 && !open) { + setInnerOffset(0); + } + const [selectedValue, setSelectedValueUnwrapped] = useControlled({ controlled: value, default: defaultValue, @@ -64,22 +74,32 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const setSelectedIndex = useEventCallback((index: number | null) => { const nextValue = index === null ? '' : labelsRef.current[index] || ''; - setSelectedIndexUnwrapped(index); setSelectedValueUnwrapped(nextValue); onValueChange?.(nextValue); + + // Wait for any close animations to finish before updating the selected index so that the + // inner item anchoring is delayed until the popup is closed. + selectedDelayedIndexRef.current = index; }); const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, animated); const runOnceAnimationsFinish = useAnimationsFinished(popupRef); + const setOpen = useEventCallback((nextOpen: boolean, event?: Event) => { onOpenChange?.(nextOpen, event); setOpenUnwrapped(nextOpen); + + function handleUnmounted() { + setMounted(false); + setSelectedIndexUnwrapped(selectedDelayedIndexRef.current); + } + if (!nextOpen) { if (animated) { - runOnceAnimationsFinish(() => setMounted(false)); + runOnceAnimationsFinish(handleUnmounted); } else { - setMounted(false); + handleUnmounted(); } } }); @@ -119,19 +139,26 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const typeahead = useTypeahead(floatingRootContext, { listRef: labelsRef, activeIndex, - resetMs: 350, + resetMs: 500, onMatch: open ? setActiveIndex : setSelectedIndex, onTypingChange(typing) { typingRef.current = typing; }, }); + const innerOffsetInteractionProps = useInnerOffset(floatingRootContext, { + onChange: setInnerOffset, + overflowRef, + scrollRef: popupRef, + }); + const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ click, dismiss, role, listNavigation, typeahead, + innerOffsetInteractionProps, ]); const getTriggerProps = React.useCallback( @@ -139,7 +166,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R [getReferenceProps], ); - const getPositionerProps = React.useCallback( + const getPopupProps = React.useCallback( (externalProps?: GenericHTMLProps) => getFloatingProps(externalProps), [getFloatingProps], ); @@ -156,7 +183,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setTriggerElement, getTriggerProps, setPositionerElement, - getPositionerProps, + getPopupProps, getItemProps, elementsRef, labelsRef, @@ -166,6 +193,9 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R open, setOpen, typingRef, + selectionRef, + overflowRef, + innerOffset, }), [ activeIndex, @@ -175,12 +205,13 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R floatingRootContext, triggerElement, getTriggerProps, - getPositionerProps, + getPopupProps, getItemProps, mounted, transitionStatus, open, setOpen, + innerOffset, ], ); } @@ -216,6 +247,7 @@ export namespace useSelectRoot { value?: string; onValueChange?: (value: string) => void; defaultValue?: string; + anchorToItem?: boolean; } export interface ReturnValue { @@ -226,7 +258,7 @@ export namespace useSelectRoot { selectedValue: string | null; floatingRootContext: FloatingRootContext; getItemProps: UseInteractionsReturn['getItemProps']; - getPositionerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + getPopupProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; elementsRef: React.MutableRefObject<(HTMLElement | null)[]>; labelsRef: React.MutableRefObject<(string | null)[]>; @@ -239,5 +271,11 @@ export namespace useSelectRoot { transitionStatus: 'entering' | 'exiting' | undefined; triggerElement: HTMLElement | null; typingRef: React.MutableRefObject; + selectionRef: React.MutableRefObject<{ + mouseUp: boolean; + select: boolean; + }>; + innerOffset: number; + overflowRef: React.MutableRefObject; } } diff --git a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts index 0fd46a7dc..3e2914123 100644 --- a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts +++ b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts @@ -1,12 +1,12 @@ 'use client'; import * as React from 'react'; -import { getTarget } from '@floating-ui/react/utils'; +import { contains } from '@floating-ui/react/utils'; import { useButton } from '../../useButton/useButton'; import type { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { ownerDocument } from '../../utils/owner'; import { useForkRef } from '../../utils/useForkRef'; import { useSelectRootContext } from '../Root/SelectRootContext'; +import { ownerDocument } from '../../utils/owner'; /** * @@ -19,10 +19,11 @@ export function useSelectTrigger( ): useSelectTrigger.ReturnValue { const { disabled = false, rootRef: externalRef } = parameters; - const { selectedValue, open, setOpen, setClickAndDragEnabled, setTriggerElement } = + const { selectedValue, open, setOpen, setTriggerElement, selectionRef, popupRef } = useSelectRootContext(); const triggerRef = React.useRef(null); + const timeoutRef = React.useRef(-1); const mergedRef = useForkRef(externalRef, triggerRef); @@ -33,11 +34,26 @@ export function useSelectTrigger( }); const handleRef = useForkRef(buttonRootRef, setTriggerElement); - const ignoreNextClickRef = React.useRef(false); + + React.useEffect(() => { + if (open) { + timeoutRef.current = window.setTimeout(() => { + selectionRef.current.select = true; + }, 300); + + return () => { + window.clearTimeout(timeoutRef.current); + }; + } + + selectionRef.current.mouseUp = true; + selectionRef.current.select = false; + return undefined; + }, [open, selectionRef]); const getTriggerProps = React.useCallback( (externalProps?: GenericHTMLProps): GenericHTMLProps => { - return mergeReactProps<'div'>( + return mergeReactProps<'button'>( { ...externalProps, children: selectedValue ?? externalProps?.children }, { tabIndex: 0, // this is needed to make the button focused after click in Safari @@ -47,42 +63,24 @@ export function useSelectTrigger( return; } - // prevents closing the menu right after it was opened - ignoreNextClickRef.current = true; - event.preventDefault(); + const doc = ownerDocument(event.currentTarget); - setClickAndDragEnabled(true); - - const mousedownTarget = getTarget(event.nativeEvent) as Element | null; - const doc = ownerDocument(mousedownTarget); - - function handleDocumentMouseUp(mouseUpEvent: MouseEvent) { - const mouseupTarget = mouseUpEvent.target as HTMLElement; - if (mouseupTarget?.dataset?.handleMouseup === 'true') { - mouseupTarget.click(); - } else if ( - mouseupTarget !== triggerRef.current && - !triggerRef.current?.contains(mouseupTarget) - ) { - setOpen(false, mouseUpEvent); + function handleMouseUp(mouseEvent: MouseEvent) { + const target = mouseEvent.target as Element | null; + if (contains(popupRef.current, target) || contains(triggerRef.current, target)) { + return; } - setClickAndDragEnabled(false); - doc.removeEventListener('mouseup', handleDocumentMouseUp); + setOpen(false, mouseEvent); } - doc.addEventListener('mouseup', handleDocumentMouseUp); - }, - onClick() { - if (ignoreNextClickRef.current) { - ignoreNextClickRef.current = false; - } + doc.addEventListener('mouseup', handleMouseUp, { once: true }); }, }, getButtonProps(), ); }, - [selectedValue, handleRef, getButtonProps, open, setClickAndDragEnabled, setOpen], + [selectedValue, handleRef, getButtonProps, open, popupRef, setOpen], ); return React.useMemo( diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index 55fd04438..71d39b06d 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -17,6 +17,7 @@ import { type VirtualElement, type Padding, type FloatingContext, + type Middleware, } from '@floating-ui/react'; import { getSide, getAlignment } from '@floating-ui/utils'; import { useEnhancedEffect } from './useEnhancedEffect'; @@ -47,6 +48,8 @@ interface UseAnchorPositioningParameters { mounted?: boolean; trackAnchor?: boolean; nodeId?: string; + inner?: Middleware; + allowAxisFlip?: boolean; } interface UseAnchorPositioningReturnValue { @@ -86,7 +89,9 @@ export function useAnchorPositioning( arrowPadding = 5, mounted = true, trackAnchor = true, + allowAxisFlip = true, nodeId, + inner: innerMiddleware, } = params; const placement = alignment === 'center' ? side : (`${side}-${alignment}` as Placement); @@ -111,7 +116,7 @@ export function useAnchorPositioning( const flipMiddleware = flip({ ...commonCollisionProps, - fallbackAxisSideDirection, + fallbackAxisSideDirection: allowAxisFlip ? fallbackAxisSideDirection : 'none', }); const shiftMiddleware = shift({ ...commonCollisionProps, @@ -130,26 +135,29 @@ export function useAnchorPositioning( }); // https://floating-ui.com/docs/flip#combining-with-shift - if (alignment !== 'center') { - middleware.push(flipMiddleware, shiftMiddleware); - } else { - middleware.push(shiftMiddleware, flipMiddleware); + if (!innerMiddleware) { + if (alignment !== 'center') { + middleware.push(flipMiddleware, shiftMiddleware); + } else { + middleware.push(shiftMiddleware, flipMiddleware); + } } middleware.push( - size({ - ...commonCollisionProps, - apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) { - Object.entries({ - '--available-width': `${availableWidth}px`, - '--available-height': `${availableHeight}px`, - '--anchor-width': `${reference.width}px`, - '--anchor-height': `${reference.height}px`, - }).forEach(([key, value]) => { - floating.style.setProperty(key, value); - }); - }, - }), + innerMiddleware ?? + size({ + ...commonCollisionProps, + apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) { + Object.entries({ + '--available-width': `${availableWidth}px`, + '--available-height': `${availableHeight}px`, + '--anchor-width': `${reference.width}px`, + '--anchor-height': `${reference.height}px`, + }).forEach(([key, value]) => { + floating.style.setProperty(key, value); + }); + }, + }), arrow( () => ({ // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set, From 2d69dd2e3c2076312217ea53bf3133f1e959eaa9 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 19 Aug 2024 15:39:50 +1000 Subject: [PATCH 05/94] Fallback positioning, Backdrop and ItemIndicator components, tests --- .../select/SelectIntroduction/system/index.js | 25 ++-- .../SelectIntroduction/system/index.tsx | 25 ++-- .../system/index.tsx.preview | 14 +++ docs/data/base/components/select/select.md | 2 +- docs/data/base/pagesApi.js | 8 ++ docs/pages/base-ui/api/select-backdrop.json | 23 ++++ .../base-ui/api/select-item-indicator.json | 20 ++++ docs/pages/base-ui/api/select-item.json | 20 ++++ docs/pages/base-ui/api/select-popup.json | 20 ++++ docs/pages/base-ui/api/select-positioner.json | 4 +- docs/pages/base-ui/api/select-root.json | 24 ++++ .../base-ui/api/use-select-backdrop.json | 8 ++ .../base-ui/react-select/[docsTab]/index.js | 20 ++++ .../select-backdrop/select-backdrop.json | 14 +++ .../select-item-indicator.json | 13 +++ .../select-positioner/select-positioner.json | 3 - .../api-docs/select-root/select-root.json | 3 + .../use-select-backdrop.json | 1 + .../Select/Backdrop/SelectBackdrop.test.tsx | 0 .../src/Select/Backdrop/SelectBackdrop.tsx | 103 +++++++++++++++++ .../src/Select/Backdrop/useSelectBackdrop.ts | 29 +++++ .../src/Select/Item/SelectItem.test.tsx | 18 +++ .../mui-base/src/Select/Item/SelectItem.tsx | 42 ++++--- .../src/Select/Item/SelectItemContext.ts | 15 +++ .../SelectItemIndicator.test.tsx | 28 +++++ .../ItemIndicator/SelectItemIndicator.tsx | 108 ++++++++++++++++++ .../src/Select/Popup/SelectPopup.test.tsx | 18 +++ .../src/Select/Popup/useSelectPopup.ts | 19 ++- .../Positioner/SelectPositioner.test.tsx | 18 +++ .../Select/Positioner/SelectPositioner.tsx | 32 +++--- .../Positioner/SelectPositionerContext.ts | 4 +- .../Select/Positioner/useSelectPositioner.tsx | 6 + .../src/Select/Root/SelectRoot.test.tsx | 0 .../mui-base/src/Select/Root/SelectRoot.tsx | 19 ++- .../src/Select/Root/SelectRootContext.ts | 1 + .../src/Select/Root/useSelectRoot.tsx | 46 ++++++-- .../src/Select/Trigger/SelectTrigger.test.tsx | 18 +++ .../src/Select/Trigger/useSelectTrigger.ts | 12 +- packages/mui-base/src/Select/index.barrel.ts | 2 + packages/mui-base/src/Select/index.ts | 2 + .../src/Select/utils/commonStyleHooks.ts | 2 +- .../src/utils/useAnchorPositioning.ts | 40 ++++--- 42 files changed, 721 insertions(+), 108 deletions(-) create mode 100644 docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview create mode 100644 docs/pages/base-ui/api/select-backdrop.json create mode 100644 docs/pages/base-ui/api/select-item-indicator.json create mode 100644 docs/pages/base-ui/api/select-item.json create mode 100644 docs/pages/base-ui/api/select-popup.json create mode 100644 docs/pages/base-ui/api/select-root.json create mode 100644 docs/pages/base-ui/api/use-select-backdrop.json create mode 100644 docs/translations/api-docs/select-backdrop/select-backdrop.json create mode 100644 docs/translations/api-docs/select-item-indicator/select-item-indicator.json create mode 100644 docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json create mode 100644 packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx create mode 100644 packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx create mode 100644 packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts create mode 100644 packages/mui-base/src/Select/Item/SelectItem.test.tsx create mode 100644 packages/mui-base/src/Select/Item/SelectItemContext.ts create mode 100644 packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx create mode 100644 packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx create mode 100644 packages/mui-base/src/Select/Popup/SelectPopup.test.tsx create mode 100644 packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx create mode 100644 packages/mui-base/src/Select/Root/SelectRoot.test.tsx create mode 100644 packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 6754e11c6..2b3ba86e6 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -7,23 +7,14 @@ export default function UnstyledSelectIntroduction() { return ( Trigger - + + {[...Array(100)].map((_, index) => ( - ( -
    - Item {index + 1}{' '} - {state.selected && } -
    - )} - /> + + Item {index + 1} + } /> + ))}
    @@ -50,6 +41,7 @@ const SelectPopup = styled(Select.Popup)` 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.1); max-height: var(--available-height); + outline: 0; `; const SelectItem = styled(Select.Item)` @@ -65,12 +57,13 @@ const SelectItem = styled(Select.Item)` line-height: 1.5; &[data-highlighted], + &[data-select='closed'][data-selected], &:focus { background-color: black; color: white; } `; -const SelectCheck = styled(Check)` +const SelectItemIndicator = styled(Select.ItemIndicator)` margin-left: 8px; `; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 6754e11c6..2b3ba86e6 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -7,23 +7,14 @@ export default function UnstyledSelectIntroduction() { return ( Trigger - + + {[...Array(100)].map((_, index) => ( - ( -
    - Item {index + 1}{' '} - {state.selected && } -
    - )} - /> + + Item {index + 1} + } /> + ))}
    @@ -50,6 +41,7 @@ const SelectPopup = styled(Select.Popup)` 0 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.1); max-height: var(--available-height); + outline: 0; `; const SelectItem = styled(Select.Item)` @@ -65,12 +57,13 @@ const SelectItem = styled(Select.Item)` line-height: 1.5; &[data-highlighted], + &[data-select='closed'][data-selected], &:focus { background-color: black; color: white; } `; -const SelectCheck = styled(Check)` +const SelectItemIndicator = styled(Select.ItemIndicator)` margin-left: 8px; `; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview new file mode 100644 index 000000000..a94386c44 --- /dev/null +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview @@ -0,0 +1,14 @@ + + Trigger + + + + {[...Array(100)].map((_, index) => ( + + Item {index + 1} + } /> + + ))} + + + \ No newline at end of file diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md index a8a384eeb..23b5da006 100644 --- a/docs/data/base/components/select/select.md +++ b/docs/data/base/components/select/select.md @@ -1,7 +1,7 @@ --- productId: base-ui title: React Select components and hook -components: SelectRoot, SelectTrigger, SelectPositioner, SelectPopup, SelectItem +components: SelectRoot, SelectTrigger, SelectBackdrop, SelectPositioner, SelectPopup, SelectItem, SelectItemIndicator githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 4423259c6..754f2e554 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -224,10 +224,18 @@ module.exports = [ pathname: '/base-ui/react-progress/components-api/#progress-track', title: 'ProgressTrack', }, + { + pathname: '/base-ui/react-select/components-api/#select-backdrop', + title: 'SelectBackdrop', + }, { pathname: '/base-ui/react-select/components-api/#select-item', title: 'SelectItem', }, + { + pathname: '/base-ui/react-select/components-api/#select-item-indicator', + title: 'SelectItemIndicator', + }, { pathname: '/base-ui/react-select/components-api/#select-popup', title: 'SelectPopup', diff --git a/docs/pages/base-ui/api/select-backdrop.json b/docs/pages/base-ui/api/select-backdrop.json new file mode 100644 index 000000000..496fa7a55 --- /dev/null +++ b/docs/pages/base-ui/api/select-backdrop.json @@ -0,0 +1,23 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
    | string" } }, + "container": { + "type": { "name": "union", "description": "HTML element
    | func" }, + "default": "false" + }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
    | func" } } + }, + "name": "SelectBackdrop", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectBackdrop = Select.Backdrop;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": null, + "muiName": "SelectBackdrop", + "filename": "/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-item-indicator.json b/docs/pages/base-ui/api/select-item-indicator.json new file mode 100644 index 000000000..18b9f2401 --- /dev/null +++ b/docs/pages/base-ui/api/select-item-indicator.json @@ -0,0 +1,20 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
    | string" } }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
    | func" } } + }, + "name": "SelectItemIndicator", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectItemIndicator = Select.ItemIndicator;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectItemIndicator", + "forwardsRefTo": "HTMLSpanElement", + "filename": "/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-item.json b/docs/pages/base-ui/api/select-item.json new file mode 100644 index 000000000..573cdfd1c --- /dev/null +++ b/docs/pages/base-ui/api/select-item.json @@ -0,0 +1,20 @@ +{ + "props": { + "closeOnClick": { "type": { "name": "bool" }, "default": "true" }, + "disabled": { "type": { "name": "bool" }, "default": "false" }, + "id": { "type": { "name": "string" } }, + "label": { "type": { "name": "string" } }, + "onClick": { "type": { "name": "func" } } + }, + "name": "SelectItem", + "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectItem = Select.Item;"], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectItem", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/Item/SelectItem.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-popup.json b/docs/pages/base-ui/api/select-popup.json new file mode 100644 index 000000000..4d86fe4bf --- /dev/null +++ b/docs/pages/base-ui/api/select-popup.json @@ -0,0 +1,20 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
    | string" } }, + "id": { "type": { "name": "string" } }, + "render": { "type": { "name": "union", "description": "element
    | func" } } + }, + "name": "SelectPopup", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectPopup = Select.Popup;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectPopup", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/Popup/SelectPopup.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-positioner.json b/docs/pages/base-ui/api/select-positioner.json index 1937b2bb5..7f1d64fba 100644 --- a/docs/pages/base-ui/api/select-positioner.json +++ b/docs/pages/base-ui/api/select-positioner.json @@ -14,7 +14,6 @@ "description": "HTML element
    | object
    | func" } }, - "anchorToItem": { "type": { "name": "bool" }, "default": "false" }, "arrowPadding": { "type": { "name": "number" }, "default": "5" }, "className": { "type": { "name": "union", "description": "func
    | string" } }, "collisionBoundary": { @@ -54,7 +53,10 @@ "import * as Select from '@base_ui/react/Select';\nconst SelectPositioner = Select.Positioner;" ], "classes": [], + "spread": true, + "themeDefaultProps": true, "muiName": "SelectPositioner", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/select-root.json b/docs/pages/base-ui/api/select-root.json new file mode 100644 index 000000000..f82c43eda --- /dev/null +++ b/docs/pages/base-ui/api/select-root.json @@ -0,0 +1,24 @@ +{ + "props": { + "alignMethod": { + "type": { "name": "enum", "description": "'selected-item'
    | 'trigger'" }, + "default": "'selected-item'" + }, + "animated": { "type": { "name": "bool" }, "default": "true" }, + "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, + "disabled": { "type": { "name": "bool" }, "default": "false" }, + "loop": { "type": { "name": "bool" }, "default": "true" }, + "onOpenChange": { "type": { "name": "func" } }, + "open": { "type": { "name": "bool" } } + }, + "name": "SelectRoot", + "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectRoot = Select.Root;"], + "classes": [], + "spread": true, + "themeDefaultProps": null, + "muiName": "SelectRoot", + "filename": "/packages/mui-base/src/Select/Root/SelectRoot.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/use-select-backdrop.json b/docs/pages/base-ui/api/use-select-backdrop.json new file mode 100644 index 000000000..1df096cb5 --- /dev/null +++ b/docs/pages/base-ui/api/use-select-backdrop.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useSelectBackdrop", + "filename": "/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts", + "imports": ["import { useSelectBackdrop } from '@base_ui/react/Select';"], + "demos": "
      " +} diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js index 8fddcfb73..20d8634a2 100644 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -3,7 +3,9 @@ import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; import AppFrame from 'docs/src/modules/components/AppFrame'; import * as pageProps from 'docs-base/data/base/components/select/select.md?@mui/markdown'; import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import SelectBackdropApiJsonPageContent from '../../api/select-backdrop.json'; import SelectItemApiJsonPageContent from '../../api/select-item.json'; +import SelectItemIndicatorApiJsonPageContent from '../../api/select-item-indicator.json'; import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; import SelectPositionerApiJsonPageContent from '../../api/select-positioner.json'; import SelectRootApiJsonPageContent from '../../api/select-root.json'; @@ -26,6 +28,13 @@ export const getStaticPaths = () => { }; export const getStaticProps = () => { + const SelectBackdropApiReq = require.context( + 'docs-base/translations/api-docs/select-backdrop', + false, + /\.\/select-backdrop.*.json$/, + ); + const SelectBackdropApiDescriptions = mapApiPageTranslations(SelectBackdropApiReq); + const SelectItemApiReq = require.context( 'docs-base/translations/api-docs/select-item', false, @@ -33,6 +42,13 @@ export const getStaticProps = () => { ); const SelectItemApiDescriptions = mapApiPageTranslations(SelectItemApiReq); + const SelectItemIndicatorApiReq = require.context( + 'docs-base/translations/api-docs/select-item-indicator', + false, + /\.\/select-item-indicator.*.json$/, + ); + const SelectItemIndicatorApiDescriptions = mapApiPageTranslations(SelectItemIndicatorApiReq); + const SelectPopupApiReq = require.context( 'docs-base/translations/api-docs/select-popup', false, @@ -64,14 +80,18 @@ export const getStaticProps = () => { return { props: { componentsApiDescriptions: { + SelectBackdrop: SelectBackdropApiDescriptions, SelectItem: SelectItemApiDescriptions, + SelectItemIndicator: SelectItemIndicatorApiDescriptions, SelectPopup: SelectPopupApiDescriptions, SelectPositioner: SelectPositionerApiDescriptions, SelectRoot: SelectRootApiDescriptions, SelectTrigger: SelectTriggerApiDescriptions, }, componentsApiPageContents: { + SelectBackdrop: SelectBackdropApiJsonPageContent, SelectItem: SelectItemApiJsonPageContent, + SelectItemIndicator: SelectItemIndicatorApiJsonPageContent, SelectPopup: SelectPopupApiJsonPageContent, SelectPositioner: SelectPositionerApiJsonPageContent, SelectRoot: SelectRootApiJsonPageContent, diff --git a/docs/translations/api-docs/select-backdrop/select-backdrop.json b/docs/translations/api-docs/select-backdrop/select-backdrop.json new file mode 100644 index 000000000..17b35bc88 --- /dev/null +++ b/docs/translations/api-docs/select-backdrop/select-backdrop.json @@ -0,0 +1,14 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "container": { "description": "The container element to which the Backdrop is appended to." }, + "keepMounted": { + "description": "If true, the Backdrop remains mounted when the Select popup is closed." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-item-indicator/select-item-indicator.json b/docs/translations/api-docs/select-item-indicator/select-item-indicator.json new file mode 100644 index 000000000..9c4340d03 --- /dev/null +++ b/docs/translations/api-docs/select-item-indicator/select-item-indicator.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "keepMounted": { + "description": "If true, the item indicator remains mounted when the item is not selected." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-positioner/select-positioner.json b/docs/translations/api-docs/select-positioner/select-positioner.json index 80f0b1a36..410bc02b7 100644 --- a/docs/translations/api-docs/select-positioner/select-positioner.json +++ b/docs/translations/api-docs/select-positioner/select-positioner.json @@ -8,9 +8,6 @@ "description": "The offset of the Select element along its alignment axis." }, "anchor": { "description": "The anchor element to which the Select popup will be placed at." }, - "anchorToItem": { - "description": "If true, positions the popup relative to the selected item inside it." - }, "arrowPadding": { "description": "Determines the padding between the arrow and the Select popup's edges. Useful when the popover popup has rounded corners via border-radius." }, diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json index aeb69c07d..469424d40 100644 --- a/docs/translations/api-docs/select-root/select-root.json +++ b/docs/translations/api-docs/select-root/select-root.json @@ -1,6 +1,9 @@ { "componentDescription": "", "propDescriptions": { + "alignMethod": { + "description": "Determines the type of alignment mode. selected-item aligns the popup so that the selected item appears over the trigger, while trigger aligns the popup using standard anchor positioning." + }, "animated": { "description": "If true, the Menu supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." }, diff --git a/docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json b/docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json new file mode 100644 index 000000000..e3eb65c6e --- /dev/null +++ b/docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json @@ -0,0 +1 @@ +{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx new file mode 100644 index 000000000..fac720dd1 --- /dev/null +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx @@ -0,0 +1,103 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { FloatingPortal } from '@floating-ui/react'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { HTMLElementType } from '../../utils/proptypes'; +import { useForkRef } from '../../utils/useForkRef'; +import { useScrollLock } from '../../utils/useScrollLock'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useSelectBackdrop } from './useSelectBackdrop'; + +const SelectBackdrop = React.forwardRef(function SelectBackdrop( + props: SelectBackdrop.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, keepMounted = false, container, ...otherProps } = props; + + const { open, mounted, innerFallback, alignMethod, selectedIndex, backdropRef } = + useSelectRootContext(); + + const { getBackdropProps } = useSelectBackdrop(); + + const mergedRef = useForkRef(backdropRef, forwardedRef); + + const ownerState: SelectBackdrop.OwnerState = React.useMemo(() => ({ open }), [open]); + + const standardMode = !( + selectedIndex !== null && + alignMethod === 'selected-item' && + !innerFallback + ); + + useScrollLock(!standardMode && mounted); + + const { renderElement } = useComponentRenderer({ + propGetter: getBackdropProps, + render: render ?? 'div', + className, + ownerState, + ref: mergedRef, + extraProps: otherProps, + }); + + const shouldRender = keepMounted || mounted; + if (!shouldRender) { + return null; + } + + return {renderElement()}; +}); + +namespace SelectBackdrop { + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + /** + * If `true`, the Backdrop remains mounted when the Select popup is closed. + * @default false + */ + keepMounted?: boolean; + /** + * The container element to which the Backdrop is appended to. + * @default false + */ + container?: HTMLElement | null | React.MutableRefObject; + } + export interface OwnerState { + open: boolean; + } +} + +SelectBackdrop.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * The container element to which the Backdrop is appended to. + * @default false + */ + container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([ + HTMLElementType, + PropTypes.func, + ]), + /** + * If `true`, the Backdrop remains mounted when the Select popup is closed. + * @default false + */ + keepMounted: PropTypes.bool, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectBackdrop }; diff --git a/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts b/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts new file mode 100644 index 000000000..c4278eb94 --- /dev/null +++ b/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts @@ -0,0 +1,29 @@ +'use client'; +import * as React from 'react'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +/** + * + * API: + * + * - [useSelectBackdrop API](https://mui.com/base-ui/api/use-select-backdrop/) + */ +export function useSelectBackdrop() { + const getBackdropProps = React.useCallback((externalProps = {}) => { + return mergeReactProps<'div'>(externalProps, { + role: 'presentation', + style: { + zIndex: 2147483647, // max z-index + overflow: 'auto', + position: 'fixed', + inset: 0, + }, + }); + }, []); + + return React.useMemo( + () => ({ + getBackdropProps, + }), + [getBackdropProps], + ); +} diff --git a/packages/mui-base/src/Select/Item/SelectItem.test.tsx b/packages/mui-base/src/Select/Item/SelectItem.test.tsx new file mode 100644 index 000000000..992c28d8f --- /dev/null +++ b/packages/mui-base/src/Select/Item/SelectItem.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Item/SelectItem.tsx b/packages/mui-base/src/Select/Item/SelectItem.tsx index 7f90a115c..e6ee9aad1 100644 --- a/packages/mui-base/src/Select/Item/SelectItem.tsx +++ b/packages/mui-base/src/Select/Item/SelectItem.tsx @@ -5,10 +5,12 @@ import { UseInteractionsReturn, useListItem } from '@floating-ui/react'; import { useSelectItem } from './useSelectItem'; import { SelectRootContext, useSelectRootContext } from '../Root/SelectRootContext'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { useId } from '../../utils/useId'; import type { BaseUIComponentProps } from '../../utils/types'; +import { useId } from '../../utils/useId'; import { useForkRef } from '../../utils/useForkRef'; import { useEventCallback } from '../../utils/useEventCallback'; +import { SelectItemContext } from './SelectItemContext'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; const InnerSelectItem = React.memo( React.forwardRef(function InnerSelectItem( @@ -28,6 +30,7 @@ const InnerSelectItem = React.memo( typingRef, handleSelect, selectionRef, + open, ...otherProps } = props; @@ -45,8 +48,8 @@ const InnerSelectItem = React.memo( }); const ownerState: SelectItem.OwnerState = React.useMemo( - () => ({ disabled, highlighted, selected }), - [disabled, highlighted, selected], + () => ({ open, disabled, highlighted, selected }), + [open, disabled, highlighted, selected], ); const { renderElement } = useComponentRenderer({ @@ -63,6 +66,7 @@ const InnerSelectItem = React.memo( return getItemProps(rootItemProps); }, extraProps: otherProps, + customStyleHookMapping: commonStyleHooks, }); return renderElement(); @@ -87,6 +91,7 @@ const SelectItem = React.forwardRef(function SelectItem( const { id: idProp, label, ...otherProps } = props; const { + open, getItemProps, activeIndex, selectedIndex, @@ -109,23 +114,28 @@ const SelectItem = React.forwardRef(function SelectItem( setSelectedIndex(listItem.index); }); + const contextValue = React.useMemo(() => ({ open, selected }), [open, selected]); + // This wrapper component is used as a performance optimization. // SelectItem reads the context and re-renders the actual SelectItem // only when it needs to. return ( - + + + ); }); @@ -140,6 +150,7 @@ interface InnerSelectItemProps extends SelectItem.Props { mouseUp: boolean; select: boolean; }>; + open: boolean; } namespace SelectItem { @@ -147,6 +158,7 @@ namespace SelectItem { disabled: boolean; highlighted: boolean; selected: boolean; + open: boolean; } export interface Props extends BaseUIComponentProps<'div', OwnerState> { diff --git a/packages/mui-base/src/Select/Item/SelectItemContext.ts b/packages/mui-base/src/Select/Item/SelectItemContext.ts new file mode 100644 index 000000000..e9ce406f9 --- /dev/null +++ b/packages/mui-base/src/Select/Item/SelectItemContext.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; + +interface SelectItemContext { + selected: boolean; +} + +export const SelectItemContext = React.createContext(null); + +export function useSelectItemContext() { + const context = React.useContext(SelectItemContext); + if (context === null) { + throw new Error('Base UI: useSelectItemContext is not defined.'); + } + return context; +} diff --git a/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx b/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx new file mode 100644 index 000000000..1f48ef7c6 --- /dev/null +++ b/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; +import { SelectItemContext } from '../Item/SelectItemContext'; + +const selectItemContextValue = { + open: true, + selected: true, +}; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLSpanElement, + render(node) { + return render( + + + + {node} + + + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx b/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx new file mode 100644 index 000000000..b4b0a9a26 --- /dev/null +++ b/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx @@ -0,0 +1,108 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import type { Side } from '@floating-ui/react'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import { useSelectItemContext } from '../Item/SelectItemContext'; + +const customStyleHookMapping: CustomStyleHookMapping = { + ...commonStyleHooks, + entering(value) { + return value ? { 'data-select-entering': '' } : null; + }, + exiting(value) { + return value ? { 'data-select-exiting': '' } : null; + }, +}; + +const SelectItemIndicator = React.forwardRef(function SelectItemIndicator( + props: SelectItemIndicator.Props, + forwardedRef: React.ForwardedRef, +) { + const { render, className, keepMounted = false, ...otherProps } = props; + + const { open, transitionStatus } = useSelectRootContext(); + const { side, alignment } = useSelectPositionerContext(); + const { selected } = useSelectItemContext(); + + const ownerState: SelectItemIndicator.OwnerState = React.useMemo( + () => ({ + entering: transitionStatus === 'entering', + exiting: transitionStatus === 'exiting', + side, + alignment, + open, + selected, + }), + [transitionStatus, side, alignment, open, selected], + ); + + const { renderElement } = useComponentRenderer({ + render: render ?? 'span', + ref: forwardedRef, + className, + ownerState, + customStyleHookMapping, + extraProps: otherProps, + }); + + const shouldRender = selected || keepMounted; + if (!shouldRender) { + return null; + } + + return renderElement(); +}); + +namespace SelectItemIndicator { + export interface Props extends BaseUIComponentProps<'span', OwnerState> { + children?: React.ReactNode; + /** + * If `true`, the item indicator remains mounted when the item is not + * selected. + * @default false + */ + keepMounted?: boolean; + } + + export interface OwnerState { + entering: boolean; + exiting: boolean; + side: Side; + alignment: 'start' | 'end' | 'center'; + open: boolean; + selected: boolean; + } +} + +SelectItemIndicator.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * If `true`, the item indicator remains mounted when the item is not + * selected. + * @default false + */ + keepMounted: PropTypes.bool, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectItemIndicator }; diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx new file mode 100644 index 000000000..14ca06177 --- /dev/null +++ b/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 779ac9bb1..709eef241 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -10,20 +10,31 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; * - [useSelectPopup API](https://mui.com/base-ui/api/use-select-popup/) */ export function useSelectPopup(): useSelectPopup.ReturnValue { - const { getPopupProps: getRootPopupProps } = useSelectRootContext(); + const { + getPopupProps: getRootPopupProps, + alignMethod, + selectedIndex, + innerFallback, + } = useSelectRootContext(); + + const hasSelectedIndex = selectedIndex !== null; const getPopupProps: useSelectPopup.ReturnValue['getPopupProps'] = React.useCallback( (externalProps = {}) => { return mergeReactProps<'div'>(getRootPopupProps(externalProps), { style: { - overflowY: 'auto', - scrollbarWidth: 'none', // must be relative to the element. position: 'relative', + overflowY: 'auto', + ...(alignMethod === 'selected-item' && + hasSelectedIndex && + !innerFallback && { + scrollbarWidth: 'none', + }), }, }); }, - [getRootPopupProps], + [getRootPopupProps, alignMethod, hasSelectedIndex, innerFallback], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx new file mode 100644 index 000000000..301f38df2 --- /dev/null +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 8df0d5c56..392d06c14 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -34,7 +34,6 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( ) { const { anchor, - anchorToItem = false, positionStrategy = 'absolute', className, render, @@ -60,10 +59,13 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( labelsRef, triggerElement, mounted, - selectedIndex, popupRef, overflowRef, innerOffset, + alignMethod, + innerFallback, + setInnerFallback, + selectedIndexOnMount, } = useSelectRootContext(); const positioner = useSelectPositioner({ @@ -83,17 +85,26 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( hideWhenDetached, sticky, allowAxisFlip: false, + innerFallback, inner: - anchorToItem && selectedIndex !== null + alignMethod === 'selected-item' && selectedIndexOnMount !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ boundary: collisionBoundary, padding: collisionPadding, listRef: elementsRef, - index: selectedIndex, + index: selectedIndexOnMount, scrollRef: popupRef, offset: innerOffset, + onFallbackChange(fallbackValue) { + setInnerFallback(fallbackValue); + if (fallbackValue && popupRef.current) { + popupRef.current.style.maxHeight = ''; + } + }, + minItemsVisible: 4, + referenceOverflowThreshold: 20, overflowRef, }) : undefined, @@ -172,13 +183,7 @@ export namespace SelectPositioner { export interface Props extends useSelectPositioner.SharedParameters, - BaseUIComponentProps<'div', OwnerState> { - /** - * If `true`, positions the popup relative to the selected item inside it. - * @default false - */ - anchorToItem?: boolean; - } + BaseUIComponentProps<'div', OwnerState> {} } SelectPositioner.propTypes /* remove-proptypes */ = { @@ -204,11 +209,6 @@ SelectPositioner.propTypes /* remove-proptypes */ = { PropTypes.object, PropTypes.func, ]), - /** - * If `true`, positions the popup relative to the selected item inside it. - * @default false - */ - anchorToItem: PropTypes.bool, /** * Determines the padding between the arrow and the Select popup's edges. Useful when the popover * popup has rounded corners via `border-radius`. diff --git a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts index e403ba580..7c78958f5 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts +++ b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts @@ -25,7 +25,9 @@ if (process.env.NODE_ENV !== 'production') { export function useSelectPositionerContext() { const context = React.useContext(SelectPositionerContext); if (context === null) { - throw new Error(' must be used within the component'); + throw new Error( + 'Base UI: must be used within the component', + ); } return context; } diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 486e71dd2..6ad67c252 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -180,6 +180,12 @@ export namespace useSelectPositioner { * @default true */ allowAxisFlip?: boolean; + /** + * Whether to use fallback anchor postioning because anchoring to an inner item results in poor + * UX. + * @default false + */ + innerFallback?: boolean; } export interface ReturnValue { diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index ecf3faefb..6c81ed64e 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -13,6 +13,7 @@ function SelectRoot(props: SelectRoot.Props) { loop = true, onOpenChange, open, + alignMethod = 'selected-item', } = props; const selectRoot = useSelectRoot({ @@ -22,14 +23,16 @@ function SelectRoot(props: SelectRoot.Props) { loop, defaultOpen, open, + alignMethod, }); const context: SelectRootContext = React.useMemo( () => ({ ...selectRoot, disabled, + alignMethod, }), - [selectRoot, disabled], + [selectRoot, disabled, alignMethod], ); return {children}; @@ -71,6 +74,13 @@ namespace SelectRoot { * @default false */ disabled?: boolean; + /** + * Determines the type of alignment mode. `selected-item` aligns the popup so that the selected + * item appears over the trigger, while `trigger` aligns the popup using standard anchor + * positioning. + * @default 'selected-item' + */ + alignMethod?: 'trigger' | 'selected-item'; } } @@ -79,6 +89,13 @@ SelectRoot.propTypes /* remove-proptypes */ = { // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ + /** + * Determines the type of alignment mode. `selected-item` aligns the popup so that the selected + * item appears over the trigger, while `trigger` aligns the popup using standard anchor + * positioning. + * @default 'selected-item' + */ + alignMethod: PropTypes.oneOf(['selected-item', 'trigger']), /** * If `true`, the Menu supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. diff --git a/packages/mui-base/src/Select/Root/SelectRootContext.ts b/packages/mui-base/src/Select/Root/SelectRootContext.ts index 91ad12d35..645da3edf 100644 --- a/packages/mui-base/src/Select/Root/SelectRootContext.ts +++ b/packages/mui-base/src/Select/Root/SelectRootContext.ts @@ -5,6 +5,7 @@ import type { useSelectRoot } from './useSelectRoot'; export interface SelectRootContext extends useSelectRoot.ReturnValue { disabled: boolean; typingRef: React.MutableRefObject; + alignMethod: 'selected-item' | 'trigger'; } export const SelectRootContext = React.createContext(null); diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index a3174c27e..d27b8913c 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -38,6 +38,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R value, onValueChange, defaultValue, + alignMethod, } = params; const [triggerElement, setTriggerElement] = React.useState(null); @@ -45,14 +46,16 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const [activeIndex, setActiveIndex] = React.useState(null); const [selectedIndex, setSelectedIndexUnwrapped] = React.useState(null); const [innerOffset, setInnerOffset] = React.useState(0); + const [innerFallback, setInnerFallback] = React.useState(false); + const [selectedIndexOnMount, setSelectedIndexOnMount] = React.useState(null); const popupRef = React.useRef(null); + const backdropRef = React.useRef(null); const typingRef = React.useRef(false); const elementsRef = React.useRef>([]); const labelsRef = React.useRef>([]); const selectionRef = React.useRef({ mouseUp: false, select: false }); const overflowRef = React.useRef({ top: 0, bottom: 0, left: 0, right: 0 }); - const selectedDelayedIndexRef = React.useRef(null); const [open, setOpenUnwrapped] = useControlled({ controlled: openParam, @@ -61,8 +64,15 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R state: 'open', }); - if (innerOffset !== 0 && !open) { - setInnerOffset(0); + if (!open) { + if (innerOffset !== 0) { + setInnerOffset(0); + } + if (innerFallback) { + setInnerFallback(false); + } + } else if (selectedIndexOnMount !== selectedIndex) { + setSelectedIndexOnMount(selectedIndex); } const [selectedValue, setSelectedValueUnwrapped] = useControlled({ @@ -76,10 +86,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const nextValue = index === null ? '' : labelsRef.current[index] || ''; setSelectedValueUnwrapped(nextValue); onValueChange?.(nextValue); - - // Wait for any close animations to finish before updating the selected index so that the - // inner item anchoring is delayed until the popup is closed. - selectedDelayedIndexRef.current = index; + setSelectedIndexUnwrapped(index); }); const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, animated); @@ -92,7 +99,6 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R function handleUnmounted() { setMounted(false); - setSelectedIndexUnwrapped(selectedDelayedIndexRef.current); } if (!nextOpen) { @@ -140,16 +146,24 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R listRef: labelsRef, activeIndex, resetMs: 500, - onMatch: open ? setActiveIndex : setSelectedIndex, + onMatch: open + ? setActiveIndex + : (index) => { + const nextValue = index === null ? '' : labelsRef.current[index] || ''; + setSelectedValueUnwrapped(nextValue); + onValueChange?.(nextValue); + setSelectedIndexUnwrapped(index); + }, onTypingChange(typing) { typingRef.current = typing; }, }); const innerOffsetInteractionProps = useInnerOffset(floatingRootContext, { + enabled: alignMethod === 'selected-item' && !innerFallback, onChange: setInnerOffset, - overflowRef, scrollRef: popupRef, + overflowRef, }); const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ @@ -177,6 +191,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setActiveIndex, selectedIndex, setSelectedIndex, + selectedIndexOnMount, selectedValue, floatingRootContext, triggerElement, @@ -190,17 +205,21 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R mounted, transitionStatus, popupRef, + backdropRef, open, setOpen, typingRef, selectionRef, overflowRef, innerOffset, + innerFallback, + setInnerFallback, }), [ activeIndex, selectedIndex, setSelectedIndex, + selectedIndexOnMount, selectedValue, floatingRootContext, triggerElement, @@ -212,6 +231,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R open, setOpen, innerOffset, + innerFallback, ], ); } @@ -247,7 +267,7 @@ export namespace useSelectRoot { value?: string; onValueChange?: (value: string) => void; defaultValue?: string; - anchorToItem?: boolean; + alignMethod?: 'trigger' | 'selected-item'; } export interface ReturnValue { @@ -255,6 +275,7 @@ export namespace useSelectRoot { setActiveIndex: React.Dispatch>; selectedIndex: number | null; setSelectedIndex: (index: number | null) => void; + selectedIndexOnMount: number | null; selectedValue: string | null; floatingRootContext: FloatingRootContext; getItemProps: UseInteractionsReturn['getItemProps']; @@ -277,5 +298,8 @@ export namespace useSelectRoot { }>; innerOffset: number; overflowRef: React.MutableRefObject; + backdropRef: React.RefObject; + innerFallback: boolean; + setInnerFallback: React.Dispatch>; } } diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx new file mode 100644 index 000000000..c6a6868c6 --- /dev/null +++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLButtonElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts index 3e2914123..0a2dd0cca 100644 --- a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts +++ b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts @@ -19,7 +19,7 @@ export function useSelectTrigger( ): useSelectTrigger.ReturnValue { const { disabled = false, rootRef: externalRef } = parameters; - const { selectedValue, open, setOpen, setTriggerElement, selectionRef, popupRef } = + const { selectedValue, open, setOpen, setTriggerElement, selectionRef, popupRef, backdropRef } = useSelectRootContext(); const triggerRef = React.useRef(null); @@ -66,8 +66,12 @@ export function useSelectTrigger( const doc = ownerDocument(event.currentTarget); function handleMouseUp(mouseEvent: MouseEvent) { - const target = mouseEvent.target as Element | null; - if (contains(popupRef.current, target) || contains(triggerRef.current, target)) { + const mouseUpTarget = mouseEvent.target as Element | null; + if ( + contains(backdropRef.current, mouseUpTarget) || + contains(popupRef.current, mouseUpTarget) || + contains(triggerRef.current, mouseUpTarget) + ) { return; } @@ -80,7 +84,7 @@ export function useSelectTrigger( getButtonProps(), ); }, - [selectedValue, handleRef, getButtonProps, open, popupRef, setOpen], + [selectedValue, handleRef, getButtonProps, open, backdropRef, popupRef, setOpen], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index d868e61ee..bc0c363c0 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -2,4 +2,6 @@ export { SelectRoot } from './Root/SelectRoot'; export { SelectTrigger } from './Trigger/SelectTrigger'; export { SelectPositioner } from './Positioner/SelectPositioner'; export { SelectPopup } from './Popup/SelectPopup'; +export { SelectBackdrop } from './Backdrop/SelectBackdrop'; export { SelectItem } from './Item/SelectItem'; +export { SelectItemIndicator } from './ItemIndicator/SelectItemIndicator'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 3148fe195..71d3adcc1 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -2,4 +2,6 @@ export { SelectRoot as Root } from './Root/SelectRoot'; export { SelectTrigger as Trigger } from './Trigger/SelectTrigger'; export { SelectPositioner as Positioner } from './Positioner/SelectPositioner'; export { SelectPopup as Popup } from './Popup/SelectPopup'; +export { SelectBackdrop as Backdrop } from './Backdrop/SelectBackdrop'; export { SelectItem as Item } from './Item/SelectItem'; +export { SelectItemIndicator as ItemIndicator } from './ItemIndicator/SelectItemIndicator'; diff --git a/packages/mui-base/src/Select/utils/commonStyleHooks.ts b/packages/mui-base/src/Select/utils/commonStyleHooks.ts index 5d51afa7f..09bde447c 100644 --- a/packages/mui-base/src/Select/utils/commonStyleHooks.ts +++ b/packages/mui-base/src/Select/utils/commonStyleHooks.ts @@ -2,4 +2,4 @@ export const commonStyleHooks = { open: (value: boolean) => ({ 'data-select': value ? 'open' : 'closed', }), -}; +} as const; diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index 71d39b06d..f38e6cc4f 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -49,6 +49,7 @@ interface UseAnchorPositioningParameters { trackAnchor?: boolean; nodeId?: string; inner?: Middleware; + innerFallback?: boolean; allowAxisFlip?: boolean; } @@ -92,8 +93,10 @@ export function useAnchorPositioning( allowAxisFlip = true, nodeId, inner: innerMiddleware, + innerFallback, } = params; + const standardMode = !(!innerFallback && innerMiddleware); const placement = alignment === 'center' ? side : (`${side}-${alignment}` as Placement); const commonCollisionProps = { @@ -108,7 +111,7 @@ export function useAnchorPositioning( const middleware: UseFloatingOptions['middleware'] = [ offset({ - mainAxis: sideOffset, + mainAxis: standardMode ? sideOffset : 0, crossAxis: alignmentOffset, alignmentAxis: alignmentOffset, }), @@ -135,7 +138,7 @@ export function useAnchorPositioning( }); // https://floating-ui.com/docs/flip#combining-with-shift - if (!innerMiddleware) { + if (standardMode) { if (alignment !== 'center') { middleware.push(flipMiddleware, shiftMiddleware); } else { @@ -144,20 +147,21 @@ export function useAnchorPositioning( } middleware.push( - innerMiddleware ?? - size({ - ...commonCollisionProps, - apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) { - Object.entries({ - '--available-width': `${availableWidth}px`, - '--available-height': `${availableHeight}px`, - '--anchor-width': `${reference.width}px`, - '--anchor-height': `${reference.height}px`, - }).forEach(([key, value]) => { - floating.style.setProperty(key, value); - }); - }, - }), + !standardMode + ? innerMiddleware + : size({ + ...commonCollisionProps, + apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) { + Object.entries({ + '--available-width': `${availableWidth}px`, + '--available-height': `${availableHeight}px`, + '--anchor-width': `${reference.width}px`, + '--anchor-height': `${reference.height}px`, + }).forEach(([key, value]) => { + floating.style.setProperty(key, value); + }); + }, + }), arrow( () => ({ // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set, @@ -167,8 +171,8 @@ export function useAnchorPositioning( }), [arrowPadding], ), - hideWhenDetached && hide(), - { + hideWhenDetached && standardMode && hide(), + standardMode && { name: 'transformOrigin', fn({ elements, middlewareData, placement: renderedPlacement }) { const currentRenderedSide = getSide(renderedPlacement); From e18131228ac0d08aa9c196b4abc8f2474056f0df Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 20 Aug 2024 08:47:59 +1000 Subject: [PATCH 06/94] Update items traversal --- .../select/SelectIntroduction/system/index.js | 4 +- .../SelectIntroduction/system/index.tsx | 4 +- .../system/index.tsx.preview | 4 +- docs/pages/base-ui/api/select-item.json | 1 + docs/pages/base-ui/api/select-root.json | 8 +- .../api-docs/select-item/select-item.json | 13 +-- .../api-docs/select-root/select-root.json | 14 ++- .../mui-base/src/Select/Item/SelectItem.tsx | 60 +++++++++---- .../mui-base/src/Select/Item/useSelectItem.ts | 9 +- .../Select/Positioner/SelectPositioner.tsx | 72 +++++++++++++-- .../mui-base/src/Select/Root/SelectRoot.tsx | 88 ++++++++++++++++--- .../src/Select/Root/SelectRootContext.ts | 6 +- .../src/Select/Root/useSelectRoot.tsx | 85 ++++++++++++------ .../src/Select/Trigger/useSelectTrigger.ts | 6 +- 14 files changed, 282 insertions(+), 92 deletions(-) diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 2b3ba86e6..d95d979e5 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -5,13 +5,13 @@ import Check from '@mui/icons-material/Check'; export default function UnstyledSelectIntroduction() { return ( - + Trigger {[...Array(100)].map((_, index) => ( - + Item {index + 1} } /> diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 2b3ba86e6..d95d979e5 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -5,13 +5,13 @@ import Check from '@mui/icons-material/Check'; export default function UnstyledSelectIntroduction() { return ( - + Trigger {[...Array(100)].map((_, index) => ( - + Item {index + 1} } /> diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview index a94386c44..5e0ffa0e2 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview @@ -1,10 +1,10 @@ - + Trigger {[...Array(100)].map((_, index) => ( - + Item {index + 1} } /> diff --git a/docs/pages/base-ui/api/select-item.json b/docs/pages/base-ui/api/select-item.json index 573cdfd1c..238bc0deb 100644 --- a/docs/pages/base-ui/api/select-item.json +++ b/docs/pages/base-ui/api/select-item.json @@ -1,5 +1,6 @@ { "props": { + "value": { "type": { "name": "string" }, "required": true }, "closeOnClick": { "type": { "name": "bool" }, "default": "true" }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "id": { "type": { "name": "string" } }, diff --git a/docs/pages/base-ui/api/select-root.json b/docs/pages/base-ui/api/select-root.json index f82c43eda..9d2b1bfe1 100644 --- a/docs/pages/base-ui/api/select-root.json +++ b/docs/pages/base-ui/api/select-root.json @@ -6,10 +6,16 @@ }, "animated": { "type": { "name": "bool" }, "default": "true" }, "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, + "defaultValue": { "type": { "name": "string" } }, "disabled": { "type": { "name": "bool" }, "default": "false" }, + "id": { "type": { "name": "string" } }, "loop": { "type": { "name": "bool" }, "default": "true" }, + "name": { "type": { "name": "string" } }, "onOpenChange": { "type": { "name": "func" } }, - "open": { "type": { "name": "bool" } } + "open": { "type": { "name": "bool" } }, + "readOnly": { "type": { "name": "bool" }, "default": "false" }, + "required": { "type": { "name": "bool" }, "default": "false" }, + "value": { "type": { "name": "string" } } }, "name": "SelectRoot", "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectRoot = Select.Root;"], diff --git a/docs/translations/api-docs/select-item/select-item.json b/docs/translations/api-docs/select-item/select-item.json index 46541ccb2..685e50293 100644 --- a/docs/translations/api-docs/select-item/select-item.json +++ b/docs/translations/api-docs/select-item/select-item.json @@ -1,15 +1,16 @@ { - "componentDescription": "An unstyled menu item to be used within a Menu.", + "componentDescription": "An unstyled select item to be used within a Select.", "propDescriptions": { "closeOnClick": { - "description": "If true, the menu will close when the menu item is clicked." + "description": "If true, the select will close when the select item is clicked." }, - "disabled": { "description": "If true, the menu item will be disabled." }, - "id": { "description": "The id of the menu item." }, + "disabled": { "description": "If true, the select item will be disabled." }, + "id": { "description": "The id of the select item." }, "label": { - "description": "A text representation of the menu item's content. Used for keyboard text navigation matching." + "description": "A text representation of the select item's content. Used for keyboard text navigation matching." }, - "onClick": { "description": "The click handler for the menu item." } + "onClick": { "description": "The click handler for the select item." }, + "value": { "description": "The value of the select item." } }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json index 469424d40..5565e95b7 100644 --- a/docs/translations/api-docs/select-root/select-root.json +++ b/docs/translations/api-docs/select-root/select-root.json @@ -5,19 +5,25 @@ "description": "Determines the type of alignment mode. selected-item aligns the popup so that the selected item appears over the trigger, while trigger aligns the popup using standard anchor positioning." }, "animated": { - "description": "If true, the Menu supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." + "description": "If true, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." }, - "defaultOpen": { "description": "If true, the Menu is initially open." }, - "disabled": { "description": "If true, the Menu is disabled." }, + "defaultOpen": { "description": "If true, the Select is initially open." }, + "defaultValue": { "description": "The default value of the select." }, + "disabled": { "description": "If true, the Select is disabled." }, + "id": { "description": "The id of the Select." }, "loop": { "description": "If true, using keyboard navigation will wrap focus to the other end of the list once the end is reached." }, + "name": { "description": "The name of the Select in the owning form." }, "onOpenChange": { "description": "Callback fired when the component requests to be opened or closed." }, "open": { "description": "Allows to control whether the dropdown is open. This is a controlled counterpart of defaultOpen." - } + }, + "readOnly": { "description": "If true, the Select is read-only." }, + "required": { "description": "If true, the Select is required." }, + "value": { "description": "The value of the select." } }, "classDescriptions": {} } diff --git a/packages/mui-base/src/Select/Item/SelectItem.tsx b/packages/mui-base/src/Select/Item/SelectItem.tsx index e6ee9aad1..0d38cfffd 100644 --- a/packages/mui-base/src/Select/Item/SelectItem.tsx +++ b/packages/mui-base/src/Select/Item/SelectItem.tsx @@ -11,6 +11,7 @@ import { useForkRef } from '../../utils/useForkRef'; import { useEventCallback } from '../../utils/useEventCallback'; import { SelectItemContext } from './SelectItemContext'; import { commonStyleHooks } from '../utils/commonStyleHooks'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; const InnerSelectItem = React.memo( React.forwardRef(function InnerSelectItem( @@ -74,44 +75,59 @@ const InnerSelectItem = React.memo( ); /** - * An unstyled menu item to be used within a Menu. + * An unstyled select item to be used within a Select. * * Demos: * - * - [Menu](https://mui.com/base-ui/react-menu/) + * - [Select](https://mui.com/base-ui/react-select/) * * API: * - * - [SelectItem API](https://mui.com/base-ui/react-menu/components-api/#menu-item) + * - [SelectItem API](https://mui.com/base-ui/react-select/components-api/#select-item) */ const SelectItem = React.forwardRef(function SelectItem( props: SelectItem.Props, forwardedRef: React.ForwardedRef, ) { - const { id: idProp, label, ...otherProps } = props; + const { id: idProp, value: valueProp, label, ...otherProps } = props; const { + setValue, open, getItemProps, activeIndex, selectedIndex, setOpen, typingRef, - setSelectedIndex, selectionRef, + valuesRef, } = useSelectRootContext(); const [item, setItem] = React.useState(null); - const listItem = useListItem({ label: label ?? item?.textContent }); + const itemLabel = label ?? item?.textContent ?? null; + const listItem = useListItem({ label: itemLabel }); const mergedRef = useForkRef(forwardedRef, listItem.ref, setItem); + useEnhancedEffect(() => { + if (listItem.index === -1) { + return undefined; + } + + const values = valuesRef.current; + values[listItem.index] = valueProp; + + return () => { + values[listItem.index] = null; + }; + }, [listItem.index, valueProp, valuesRef]); + const id = useId(idProp); const highlighted = listItem.index === activeIndex; const selected = listItem.index === selectedIndex; const handleSelect = useEventCallback(() => { - setSelectedIndex(listItem.index); + setValue(valueProp); }); const contextValue = React.useMemo(() => ({ open, selected }), [open, selected]); @@ -139,7 +155,7 @@ const SelectItem = React.forwardRef(function SelectItem( ); }); -interface InnerSelectItemProps extends SelectItem.Props { +interface InnerSelectItemProps extends Omit { highlighted: boolean; selected: boolean; getItemProps: UseInteractionsReturn['getItemProps']; @@ -164,25 +180,29 @@ namespace SelectItem { export interface Props extends BaseUIComponentProps<'div', OwnerState> { children?: React.ReactNode; /** - * The click handler for the menu item. + * The value of the select item. + */ + value: string; + /** + * The click handler for the select item. */ onClick?: React.MouseEventHandler; /** - * If `true`, the menu item will be disabled. + * If `true`, the select item will be disabled. * @default false */ disabled?: boolean; /** - * A text representation of the menu item's content. + * A text representation of the select item's content. * Used for keyboard text navigation matching. */ label?: string; /** - * The id of the menu item. + * The id of the select item. */ id?: string; /** - * If `true`, the menu will close when the menu item is clicked. + * If `true`, the select will close when the select item is clicked. * * @default true */ @@ -200,29 +220,33 @@ SelectItem.propTypes /* remove-proptypes */ = { */ children: PropTypes.node, /** - * If `true`, the menu will close when the menu item is clicked. + * If `true`, the select will close when the select item is clicked. * * @default true */ closeOnClick: PropTypes.bool, /** - * If `true`, the menu item will be disabled. + * If `true`, the select item will be disabled. * @default false */ disabled: PropTypes.bool, /** - * The id of the menu item. + * The id of the select item. */ id: PropTypes.string, /** - * A text representation of the menu item's content. + * A text representation of the select item's content. * Used for keyboard text navigation matching. */ label: PropTypes.string, /** - * The click handler for the menu item. + * The click handler for the select item. */ onClick: PropTypes.func, + /** + * The value of the select item. + */ + value: PropTypes.string.isRequired, } as any; export { SelectItem }; diff --git a/packages/mui-base/src/Select/Item/useSelectItem.ts b/packages/mui-base/src/Select/Item/useSelectItem.ts index 08c890006..621a73896 100644 --- a/packages/mui-base/src/Select/Item/useSelectItem.ts +++ b/packages/mui-base/src/Select/Item/useSelectItem.ts @@ -16,7 +16,6 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R const { disabled = false, highlighted, - selected, id, ref: externalRef, setOpen, @@ -69,11 +68,7 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R return; } - if (selected) { - if (selectionRef.current.select) { - commitSelection(event.nativeEvent); - } - } else { + if (selectionRef.current.select) { commitSelection(event.nativeEvent); } @@ -82,7 +77,7 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R }), ); }, - [commitSelection, getButtonProps, highlighted, id, selected, selectionRef, typingRef], + [commitSelection, getButtonProps, highlighted, id, selectionRef, typingRef], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 392d06c14..ea988b76f 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -16,6 +16,25 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { useForkRef } from '../../utils/useForkRef'; import { useSelectPositioner } from './useSelectPositioner'; import { HTMLElementType } from '../../utils/proptypes'; +import { visuallyHidden } from '../../utils/visuallyHidden'; +import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useId } from '../../utils/useId'; + +function findSelectItems(root: React.ReactElement) { + const selectItems: React.ReactElement[] = []; + React.Children.forEach(root.props?.children, (child) => { + if (React.isValidElement(child)) { + const childProps = child.props as any; + if (childProps?.value != null) { + selectItems.push(child); + } else if (childProps?.children) { + selectItems.push(...findSelectItems(child)); + } + } + }); + return selectItems; +} /** * Renders the element that positions the Select popup. @@ -65,9 +84,24 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( alignMethod, innerFallback, setInnerFallback, - selectedIndexOnMount, + selectedIndex, + name, + required, + disabled, + id: idProp, } = useSelectRootContext(); + const { setControlId } = useFieldRootContext(); + + const id = useId(idProp); + + useEnhancedEffect(() => { + setControlId(id); + return () => { + setControlId(undefined); + }; + }, [id, setControlId]); + const positioner = useSelectPositioner({ anchor: anchor || triggerElement, floatingRootContext, @@ -87,14 +121,14 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( allowAxisFlip: false, innerFallback, inner: - alignMethod === 'selected-item' && selectedIndexOnMount !== null + alignMethod === 'selected-item' && selectedIndex !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ boundary: collisionBoundary, padding: collisionPadding, listRef: elementsRef, - index: selectedIndexOnMount, + index: selectedIndex, scrollRef: popupRef, offset: innerOffset, onFallbackChange(fallbackValue) { @@ -150,21 +184,49 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( extraProps: otherProps, }); + const positionerElement = renderElement(); + const selectItems = findSelectItems(positionerElement); + const mountedItemsElement = keepMounted ? null : ; + const nativeSelectElement = ( + + ); + const shouldRender = keepMounted || mounted; if (!shouldRender) { - return null; + return ( + + + {nativeSelectElement} + {mountedItemsElement} + + + ); } return ( + {nativeSelectElement} - {renderElement()} + {positionerElement} diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index 6c81ed64e..f68c54572 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -7,9 +7,15 @@ import { useSelectRoot } from './useSelectRoot'; function SelectRoot(props: SelectRoot.Props) { const { animated = true, + id, + name, children, + value, + defaultValue, defaultOpen = false, disabled = false, + readOnly = false, + required = false, loop = true, onOpenChange, open, @@ -24,6 +30,8 @@ function SelectRoot(props: SelectRoot.Props) { defaultOpen, open, alignMethod, + value, + defaultValue, }); const context: SelectRootContext = React.useMemo( @@ -31,8 +39,12 @@ function SelectRoot(props: SelectRoot.Props) { ...selectRoot, disabled, alignMethod, + id, + name, + required, + readOnly, }), - [selectRoot, disabled, alignMethod], + [selectRoot, disabled, alignMethod, id, name, required, readOnly], ); return {children}; @@ -41,7 +53,7 @@ function SelectRoot(props: SelectRoot.Props) { namespace SelectRoot { export interface Props { /** - * If `true`, the Menu supports CSS-based animations and transitions. + * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. * * @default true @@ -49,7 +61,39 @@ namespace SelectRoot { animated?: boolean; children: React.ReactNode; /** - * If `true`, the Menu is initially open. + * The name of the Select in the owning form. + */ + name?: string; + /** + * The id of the Select. + */ + id?: string; + /** + * If `true`, the Select is required. + * @default false + */ + required?: boolean; + /** + * If `true`, the Select is read-only. + * @default false + */ + readOnly?: boolean; + /** + * If `true`, the Select is disabled. + * + * @default false + */ + disabled?: boolean; + /** + * The value of the select. + */ + value?: string; + /** + * The default value of the select. + */ + defaultValue?: string; + /** + * If `true`, the Select is initially open. * * @default false */ @@ -68,12 +112,6 @@ namespace SelectRoot { * This is a controlled counterpart of `defaultOpen`. */ open?: boolean; - /** - * If `true`, the Menu is disabled. - * - * @default false - */ - disabled?: boolean; /** * Determines the type of alignment mode. `selected-item` aligns the popup so that the selected * item appears over the trigger, while `trigger` aligns the popup using standard anchor @@ -97,7 +135,7 @@ SelectRoot.propTypes /* remove-proptypes */ = { */ alignMethod: PropTypes.oneOf(['selected-item', 'trigger']), /** - * If `true`, the Menu supports CSS-based animations and transitions. + * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. * * @default true @@ -108,22 +146,34 @@ SelectRoot.propTypes /* remove-proptypes */ = { */ children: PropTypes.node, /** - * If `true`, the Menu is initially open. + * If `true`, the Select is initially open. * * @default false */ defaultOpen: PropTypes.bool, /** - * If `true`, the Menu is disabled. + * The default value of the select. + */ + defaultValue: PropTypes.string, + /** + * If `true`, the Select is disabled. * * @default false */ disabled: PropTypes.bool, + /** + * The id of the Select. + */ + id: PropTypes.string, /** * If `true`, using keyboard navigation will wrap focus to the other end of the list once the end is reached. * @default true */ loop: PropTypes.bool, + /** + * The name of the Select in the owning form. + */ + name: PropTypes.string, /** * Callback fired when the component requests to be opened or closed. */ @@ -133,6 +183,20 @@ SelectRoot.propTypes /* remove-proptypes */ = { * This is a controlled counterpart of `defaultOpen`. */ open: PropTypes.bool, + /** + * If `true`, the Select is read-only. + * @default false + */ + readOnly: PropTypes.bool, + /** + * If `true`, the Select is required. + * @default false + */ + required: PropTypes.bool, + /** + * The value of the select. + */ + value: PropTypes.string, } as any; export { SelectRoot }; diff --git a/packages/mui-base/src/Select/Root/SelectRootContext.ts b/packages/mui-base/src/Select/Root/SelectRootContext.ts index 645da3edf..8a320fc1b 100644 --- a/packages/mui-base/src/Select/Root/SelectRootContext.ts +++ b/packages/mui-base/src/Select/Root/SelectRootContext.ts @@ -3,9 +3,13 @@ import * as React from 'react'; import type { useSelectRoot } from './useSelectRoot'; export interface SelectRootContext extends useSelectRoot.ReturnValue { - disabled: boolean; typingRef: React.MutableRefObject; alignMethod: 'selected-item' | 'trigger'; + id: string | undefined; + name: string | undefined; + disabled: boolean; + required: boolean; + readOnly: boolean; } export const SelectRootContext = React.createContext(null); diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index d27b8913c..e49b89567 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -1,5 +1,6 @@ 'use client'; import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import { useClick, useDismiss, @@ -18,6 +19,7 @@ import { useTransitionStatus } from '../../utils/useTransitionStatus'; import { useEventCallback } from '../../utils/useEventCallback'; import { useAnimationsFinished } from '../../utils/useAnimationsFinished'; import { useControlled } from '../../utils/useControlled'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; const EMPTY_ARRAY: never[] = []; @@ -35,7 +37,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R onOpenChange, disabled, loop, - value, + value: valueProp, onValueChange, defaultValue, alignMethod, @@ -44,7 +46,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const [triggerElement, setTriggerElement] = React.useState(null); const [positionerElement, setPositionerElement] = React.useState(null); const [activeIndex, setActiveIndex] = React.useState(null); - const [selectedIndex, setSelectedIndexUnwrapped] = React.useState(null); + const [selectedIndex, setSelectedIndex] = React.useState(null); const [innerOffset, setInnerOffset] = React.useState(0); const [innerFallback, setInnerFallback] = React.useState(false); const [selectedIndexOnMount, setSelectedIndexOnMount] = React.useState(null); @@ -54,6 +56,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const typingRef = React.useRef(false); const elementsRef = React.useRef>([]); const labelsRef = React.useRef>([]); + const valuesRef = React.useRef>([]); const selectionRef = React.useRef({ mouseUp: false, select: false }); const overflowRef = React.useRef({ top: 0, bottom: 0, left: 0, right: 0 }); @@ -64,31 +67,44 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R state: 'open', }); - if (!open) { - if (innerOffset !== 0) { - setInnerOffset(0); - } - if (innerFallback) { - setInnerFallback(false); - } - } else if (selectedIndexOnMount !== selectedIndex) { + if (open && selectedIndex !== selectedIndexOnMount) { setSelectedIndexOnMount(selectedIndex); } - const [selectedValue, setSelectedValueUnwrapped] = useControlled({ - controlled: value, + const [value, setValueUnwrapped] = useControlled({ + controlled: valueProp, default: defaultValue, name: 'Select', - state: 'selectedValue', + state: 'value', }); - const setSelectedIndex = useEventCallback((index: number | null) => { - const nextValue = index === null ? '' : labelsRef.current[index] || ''; - setSelectedValueUnwrapped(nextValue); + const [label, setLabel] = React.useState(null); + + const setValue: useSelectRoot.ReturnValue['setValue'] = useEventCallback((nextValue) => { onValueChange?.(nextValue); - setSelectedIndexUnwrapped(index); + setValueUnwrapped(nextValue); + + if (nextValue !== null) { + const index = valuesRef.current.indexOf(nextValue); + setSelectedIndex(index); + setLabel(labelsRef.current[index]); + } else { + setSelectedIndex(null); + setLabel(null); + } }); + useEnhancedEffect(() => { + // Wait for the items to have registered their values in `valuesRef`. + queueMicrotask(() => { + const index = valuesRef.current.indexOf(value); + if (index !== -1) { + setSelectedIndex(index); + setLabel(labelsRef.current[index]); + } + }); + }, [value]); + const { mounted, setMounted, transitionStatus } = useTransitionStatus(open, animated); const runOnceAnimationsFinish = useAnimationsFinished(popupRef); @@ -98,7 +114,12 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setOpenUnwrapped(nextOpen); function handleUnmounted() { - setMounted(false); + // Prevents the position from visibly changing upon selection when the Select is closed. + ReactDOM.flushSync(() => { + setMounted(false); + setInnerOffset(0); + setInnerFallback(false); + }); } if (!nextOpen) { @@ -149,10 +170,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R onMatch: open ? setActiveIndex : (index) => { - const nextValue = index === null ? '' : labelsRef.current[index] || ''; - setSelectedValueUnwrapped(nextValue); - onValueChange?.(nextValue); - setSelectedIndexUnwrapped(index); + setValue(valuesRef.current[index]); }, onTypingChange(typing) { typingRef.current = typing; @@ -187,12 +205,15 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R return React.useMemo( () => ({ + value, + setValue, + label, + setLabel, activeIndex, setActiveIndex, selectedIndex, setSelectedIndex, selectedIndexOnMount, - selectedValue, floatingRootContext, triggerElement, setTriggerElement, @@ -202,6 +223,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R getItemProps, elementsRef, labelsRef, + valuesRef, mounted, transitionStatus, popupRef, @@ -216,11 +238,12 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setInnerFallback, }), [ + value, + label, + setValue, activeIndex, selectedIndex, - setSelectedIndex, selectedIndexOnMount, - selectedValue, floatingRootContext, triggerElement, getTriggerProps, @@ -264,25 +287,29 @@ export namespace useSelectRoot { * If `true`, the Select is disabled. */ disabled: boolean; - value?: string; - onValueChange?: (value: string) => void; + value?: string | null; + onValueChange?: (value: string | null) => void; defaultValue?: string; alignMethod?: 'trigger' | 'selected-item'; } export interface ReturnValue { + value: string | null; + setValue: (value: string | null) => void; + label: string | null; + setLabel: React.Dispatch>; activeIndex: number | null; setActiveIndex: React.Dispatch>; selectedIndex: number | null; - setSelectedIndex: (index: number | null) => void; + setSelectedIndex: React.Dispatch>; selectedIndexOnMount: number | null; - selectedValue: string | null; floatingRootContext: FloatingRootContext; getItemProps: UseInteractionsReturn['getItemProps']; getPopupProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; elementsRef: React.MutableRefObject<(HTMLElement | null)[]>; labelsRef: React.MutableRefObject<(string | null)[]>; + valuesRef: React.MutableRefObject<(string | null)[]>; mounted: boolean; open: boolean; popupRef: React.RefObject; diff --git a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts index 0a2dd0cca..df4210a40 100644 --- a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts +++ b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts @@ -19,7 +19,7 @@ export function useSelectTrigger( ): useSelectTrigger.ReturnValue { const { disabled = false, rootRef: externalRef } = parameters; - const { selectedValue, open, setOpen, setTriggerElement, selectionRef, popupRef, backdropRef } = + const { open, setOpen, setTriggerElement, selectionRef, popupRef, backdropRef, label } = useSelectRootContext(); const triggerRef = React.useRef(null); @@ -54,7 +54,7 @@ export function useSelectTrigger( const getTriggerProps = React.useCallback( (externalProps?: GenericHTMLProps): GenericHTMLProps => { return mergeReactProps<'button'>( - { ...externalProps, children: selectedValue ?? externalProps?.children }, + { ...externalProps, children: label ?? externalProps?.children }, { tabIndex: 0, // this is needed to make the button focused after click in Safari ref: handleRef, @@ -84,7 +84,7 @@ export function useSelectTrigger( getButtonProps(), ); }, - [selectedValue, handleRef, getButtonProps, open, backdropRef, popupRef, setOpen], + [label, handleRef, getButtonProps, open, backdropRef, popupRef, setOpen], ); return React.useMemo( From 8a49b3dda37ed6ed55f06a9dc21707a70e6080ce Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 23 Aug 2024 16:24:19 +1000 Subject: [PATCH 07/94] Fix lint --- .../base/components/select/SelectIntroduction/system/index.js | 4 ++-- .../components/select/SelectIntroduction/system/index.tsx | 4 ++-- packages/mui-base/src/Select/Item/SelectItem.test.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index d95d979e5..40a1ad473 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -38,8 +38,8 @@ const SelectPopup = styled(Select.Popup)` padding: 4px; border-radius: 5px; box-shadow: - 0 2px 4px rgba(0, 0, 0, 0.1), - 0 0 0 1px rgba(0, 0, 0, 0.1); + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; `; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index d95d979e5..40a1ad473 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -38,8 +38,8 @@ const SelectPopup = styled(Select.Popup)` padding: 4px; border-radius: 5px; box-shadow: - 0 2px 4px rgba(0, 0, 0, 0.1), - 0 0 0 1px rgba(0, 0, 0, 0.1); + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; `; diff --git a/packages/mui-base/src/Select/Item/SelectItem.test.tsx b/packages/mui-base/src/Select/Item/SelectItem.test.tsx index 992c28d8f..5b55d502d 100644 --- a/packages/mui-base/src/Select/Item/SelectItem.test.tsx +++ b/packages/mui-base/src/Select/Item/SelectItem.test.tsx @@ -5,7 +5,7 @@ import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( From aa4316b410621d659c68163784422580fb3acfa3 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 26 Aug 2024 11:25:07 +1000 Subject: [PATCH 08/94] Add grouping --- .../select/SelectIntroduction/system/index.js | 45 +++++++++-- .../SelectIntroduction/system/index.tsx | 45 +++++++++-- .../system/index.tsx.preview | 14 ---- docs/data/base/components/select/select.md | 2 +- docs/data/base/pagesApi.js | 8 ++ docs/pages/base-ui/api/select-backdrop.json | 3 +- .../pages/base-ui/api/select-group-label.json | 19 +++++ docs/pages/base-ui/api/select-group.json | 19 +++++ .../base-ui/react-select/[docsTab]/index.js | 20 +++++ .../select-group-label.json | 10 +++ .../api-docs/select-group/select-group.json | 10 +++ .../Select/Backdrop/SelectBackdrop.test.tsx | 18 +++++ .../src/Select/Group/SelectGroup.test.tsx | 18 +++++ .../mui-base/src/Select/Group/SelectGroup.tsx | 75 +++++++++++++++++++ .../src/Select/Group/SelectGroupContext.ts | 16 ++++ .../GroupLabel/SelectGroupLabel.test.tsx | 18 +++++ .../Select/GroupLabel/SelectGroupLabel.tsx | 74 ++++++++++++++++++ packages/mui-base/src/Select/index.barrel.ts | 2 + packages/mui-base/src/Select/index.ts | 2 + 19 files changed, 392 insertions(+), 26 deletions(-) delete mode 100644 docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview create mode 100644 docs/pages/base-ui/api/select-group-label.json create mode 100644 docs/pages/base-ui/api/select-group.json create mode 100644 docs/translations/api-docs/select-group-label/select-group-label.json create mode 100644 docs/translations/api-docs/select-group/select-group.json create mode 100644 packages/mui-base/src/Select/Group/SelectGroup.test.tsx create mode 100644 packages/mui-base/src/Select/Group/SelectGroup.tsx create mode 100644 packages/mui-base/src/Select/Group/SelectGroupContext.ts create mode 100644 packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx create mode 100644 packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 40a1ad473..29555e57b 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -3,6 +3,29 @@ import * as Select from '@base_ui/react/Select'; import { styled } from '@mui/system'; import Check from '@mui/icons-material/Check'; +const data = { + Fruits: [ + { + value: 'apple', + label: 'Apple', + }, + { + value: 'banana', + label: 'Banana', + }, + ], + Vegetables: [ + { + value: 'carrot', + label: 'Carrot', + }, + { + value: 'lettuce', + label: 'Lettuce', + }, + ], +}; + export default function UnstyledSelectIntroduction() { return ( @@ -10,11 +33,16 @@ export default function UnstyledSelectIntroduction() { - {[...Array(100)].map((_, index) => ( - - Item {index + 1} - } /> - + {Object.entries(data).map(([group, items]) => ( + + {group} + {items.map((item) => ( + + {item.label} + } /> + + ))} + ))} @@ -67,3 +95,10 @@ const SelectItem = styled(Select.Item)` const SelectItemIndicator = styled(Select.ItemIndicator)` margin-left: 8px; `; + +const SelectGroupLabel = styled(Select.GroupLabel)` + font-weight: bold; + padding: 4px 12px; + cursor: default; + user-select: none; +`; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 40a1ad473..29555e57b 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -3,6 +3,29 @@ import * as Select from '@base_ui/react/Select'; import { styled } from '@mui/system'; import Check from '@mui/icons-material/Check'; +const data = { + Fruits: [ + { + value: 'apple', + label: 'Apple', + }, + { + value: 'banana', + label: 'Banana', + }, + ], + Vegetables: [ + { + value: 'carrot', + label: 'Carrot', + }, + { + value: 'lettuce', + label: 'Lettuce', + }, + ], +}; + export default function UnstyledSelectIntroduction() { return ( @@ -10,11 +33,16 @@ export default function UnstyledSelectIntroduction() { - {[...Array(100)].map((_, index) => ( - - Item {index + 1} - } /> - + {Object.entries(data).map(([group, items]) => ( + + {group} + {items.map((item) => ( + + {item.label} + } /> + + ))} + ))} @@ -67,3 +95,10 @@ const SelectItem = styled(Select.Item)` const SelectItemIndicator = styled(Select.ItemIndicator)` margin-left: 8px; `; + +const SelectGroupLabel = styled(Select.GroupLabel)` + font-weight: bold; + padding: 4px 12px; + cursor: default; + user-select: none; +`; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview b/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview deleted file mode 100644 index 5e0ffa0e2..000000000 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ - - Trigger - - - - {[...Array(100)].map((_, index) => ( - - Item {index + 1} - } /> - - ))} - - - \ No newline at end of file diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md index 23b5da006..d66a6faf4 100644 --- a/docs/data/base/components/select/select.md +++ b/docs/data/base/components/select/select.md @@ -1,7 +1,7 @@ --- productId: base-ui title: React Select components and hook -components: SelectRoot, SelectTrigger, SelectBackdrop, SelectPositioner, SelectPopup, SelectItem, SelectItemIndicator +components: SelectRoot, SelectTrigger, SelectBackdrop, SelectPositioner, SelectPopup, SelectItem, SelectItemIndicator, SelectGroup, SelectGroupLabel githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 754f2e554..de4a5ce9c 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -228,6 +228,14 @@ module.exports = [ pathname: '/base-ui/react-select/components-api/#select-backdrop', title: 'SelectBackdrop', }, + { + pathname: '/base-ui/react-select/components-api/#select-group', + title: 'SelectGroup', + }, + { + pathname: '/base-ui/react-select/components-api/#select-group-label', + title: 'SelectGroupLabel', + }, { pathname: '/base-ui/react-select/components-api/#select-item', title: 'SelectItem', diff --git a/docs/pages/base-ui/api/select-backdrop.json b/docs/pages/base-ui/api/select-backdrop.json index 496fa7a55..ca4f23bb3 100644 --- a/docs/pages/base-ui/api/select-backdrop.json +++ b/docs/pages/base-ui/api/select-backdrop.json @@ -14,8 +14,9 @@ ], "classes": [], "spread": true, - "themeDefaultProps": null, + "themeDefaultProps": true, "muiName": "SelectBackdrop", + "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/select-group-label.json b/docs/pages/base-ui/api/select-group-label.json new file mode 100644 index 000000000..8f66b91cb --- /dev/null +++ b/docs/pages/base-ui/api/select-group-label.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
      | string" } }, + "render": { "type": { "name": "union", "description": "element
      | func" } } + }, + "name": "SelectGroupLabel", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectGroupLabel = Select.GroupLabel;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectGroupLabel", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-group.json b/docs/pages/base-ui/api/select-group.json new file mode 100644 index 000000000..5b6fcb0a7 --- /dev/null +++ b/docs/pages/base-ui/api/select-group.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
      | string" } }, + "render": { "type": { "name": "union", "description": "element
      | func" } } + }, + "name": "SelectGroup", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectGroup = Select.Group;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectGroup", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/Group/SelectGroup.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js index 20d8634a2..9cd15c9c6 100644 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -4,6 +4,8 @@ import AppFrame from 'docs/src/modules/components/AppFrame'; import * as pageProps from 'docs-base/data/base/components/select/select.md?@mui/markdown'; import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; import SelectBackdropApiJsonPageContent from '../../api/select-backdrop.json'; +import SelectGroupApiJsonPageContent from '../../api/select-group.json'; +import SelectGroupLabelApiJsonPageContent from '../../api/select-group-label.json'; import SelectItemApiJsonPageContent from '../../api/select-item.json'; import SelectItemIndicatorApiJsonPageContent from '../../api/select-item-indicator.json'; import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; @@ -35,6 +37,20 @@ export const getStaticProps = () => { ); const SelectBackdropApiDescriptions = mapApiPageTranslations(SelectBackdropApiReq); + const SelectGroupApiReq = require.context( + 'docs-base/translations/api-docs/select-group', + false, + /\.\/select-group.*.json$/, + ); + const SelectGroupApiDescriptions = mapApiPageTranslations(SelectGroupApiReq); + + const SelectGroupLabelApiReq = require.context( + 'docs-base/translations/api-docs/select-group-label', + false, + /\.\/select-group-label.*.json$/, + ); + const SelectGroupLabelApiDescriptions = mapApiPageTranslations(SelectGroupLabelApiReq); + const SelectItemApiReq = require.context( 'docs-base/translations/api-docs/select-item', false, @@ -81,6 +97,8 @@ export const getStaticProps = () => { props: { componentsApiDescriptions: { SelectBackdrop: SelectBackdropApiDescriptions, + SelectGroup: SelectGroupApiDescriptions, + SelectGroupLabel: SelectGroupLabelApiDescriptions, SelectItem: SelectItemApiDescriptions, SelectItemIndicator: SelectItemIndicatorApiDescriptions, SelectPopup: SelectPopupApiDescriptions, @@ -90,6 +108,8 @@ export const getStaticProps = () => { }, componentsApiPageContents: { SelectBackdrop: SelectBackdropApiJsonPageContent, + SelectGroup: SelectGroupApiJsonPageContent, + SelectGroupLabel: SelectGroupLabelApiJsonPageContent, SelectItem: SelectItemApiJsonPageContent, SelectItemIndicator: SelectItemIndicatorApiJsonPageContent, SelectPopup: SelectPopupApiJsonPageContent, diff --git a/docs/translations/api-docs/select-group-label/select-group-label.json b/docs/translations/api-docs/select-group-label/select-group-label.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/translations/api-docs/select-group-label/select-group-label.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-group/select-group.json b/docs/translations/api-docs/select-group/select-group.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/translations/api-docs/select-group/select-group.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx index e69de29bb..75e9c32e0 100644 --- a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Group/SelectGroup.test.tsx b/packages/mui-base/src/Select/Group/SelectGroup.test.tsx new file mode 100644 index 000000000..22416da9f --- /dev/null +++ b/packages/mui-base/src/Select/Group/SelectGroup.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Group/SelectGroup.tsx b/packages/mui-base/src/Select/Group/SelectGroup.tsx new file mode 100644 index 000000000..1e0fd82e2 --- /dev/null +++ b/packages/mui-base/src/Select/Group/SelectGroup.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { SelectGroupContext } from './SelectGroupContext'; + +const SelectGroup = React.forwardRef(function SelectGroup( + props: SelectGroup.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ...otherProps } = props; + + const [labelId, setLabelId] = React.useState(); + + const ownerState: SelectGroup.OwnerState = React.useMemo(() => ({}), []); + + const getSelectGroupProps = React.useCallback( + (externalProps = {}) => + mergeReactProps(externalProps, { + role: 'group', + 'aria-labelledby': labelId, + }), + [labelId], + ); + + const contextValue: SelectGroupContext = React.useMemo( + () => ({ + labelId, + setLabelId, + }), + [labelId, setLabelId], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getSelectGroupProps, + render: render ?? 'div', + ref: forwardedRef, + ownerState, + className, + extraProps: otherProps, + }); + + return ( + + {renderElement()} + + ); +}); + +SelectGroup.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +namespace SelectGroup { + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} + +export { SelectGroup }; diff --git a/packages/mui-base/src/Select/Group/SelectGroupContext.ts b/packages/mui-base/src/Select/Group/SelectGroupContext.ts new file mode 100644 index 000000000..1cd08b6f8 --- /dev/null +++ b/packages/mui-base/src/Select/Group/SelectGroupContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; + +export interface SelectGroupContext { + labelId: string | undefined; + setLabelId: React.Dispatch>; +} + +export const SelectGroupContext = React.createContext(null); + +export function useSelectGroupContext() { + const context = React.useContext(SelectGroupContext); + if (context === null) { + throw new Error('Base UI: must be used within a '); + } + return context; +} diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx new file mode 100644 index 000000000..07f42abc6 --- /dev/null +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx new file mode 100644 index 000000000..60357d20a --- /dev/null +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useId } from '../../utils/useId'; +import { useSelectGroupContext } from '../Group/SelectGroupContext'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; + +const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( + props: SelectGroupLabel.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, id: idProp, ...otherProps } = props; + + const { setLabelId } = useSelectGroupContext(); + + const ownerState: SelectGroupLabel.OwnerState = React.useMemo(() => ({}), []); + + const id = useId(idProp); + + useEnhancedEffect(() => { + setLabelId(id); + }, [id, setLabelId]); + + const getSelectGroupLabelProps = React.useCallback( + (externalProps = {}) => + mergeReactProps(externalProps, { + id, + }), + [id], + ); + + const { renderElement } = useComponentRenderer({ + propGetter: getSelectGroupLabelProps, + render: render ?? 'div', + ref: forwardedRef, + ownerState, + className, + extraProps: otherProps, + }); + + return renderElement(); +}); + +SelectGroupLabel.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * @ignore + */ + id: PropTypes.string, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +namespace SelectGroupLabel { + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} + +export { SelectGroupLabel }; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index bc0c363c0..7f27a76d5 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -5,3 +5,5 @@ export { SelectPopup } from './Popup/SelectPopup'; export { SelectBackdrop } from './Backdrop/SelectBackdrop'; export { SelectItem } from './Item/SelectItem'; export { SelectItemIndicator } from './ItemIndicator/SelectItemIndicator'; +export { SelectGroup } from './Group/SelectGroup'; +export { SelectGroupLabel } from './GroupLabel/SelectGroupLabel'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 71d3adcc1..9709de6ad 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -5,3 +5,5 @@ export { SelectPopup as Popup } from './Popup/SelectPopup'; export { SelectBackdrop as Backdrop } from './Backdrop/SelectBackdrop'; export { SelectItem as Item } from './Item/SelectItem'; export { SelectItemIndicator as ItemIndicator } from './ItemIndicator/SelectItemIndicator'; +export { SelectGroup as Group } from './Group/SelectGroup'; +export { SelectGroupLabel as GroupLabel } from './GroupLabel/SelectGroupLabel'; From 21a75d0e3cf56ec0e6d4ac1bca375074a08253cd Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 26 Aug 2024 16:29:35 +1000 Subject: [PATCH 09/94] Rename components, handle default value --- .../select/SelectIntroduction/system/index.js | 39 +++++++----- .../SelectIntroduction/system/index.tsx | 39 +++++++----- docs/data/base/components/select/select.md | 59 ++++++++++++++++++- docs/data/base/pagesApi.js | 16 ++--- ...up.json => select-option-group-label.json} | 8 +-- ...up-label.json => select-option-group.json} | 8 +-- ...ator.json => select-option-indicator.json} | 8 +-- .../{select-item.json => select-option.json} | 10 ++-- docs/pages/base-ui/api/select-root.json | 5 +- docs/pages/base-ui/api/use-select-option.json | 8 +++ .../base-ui/react-select/[docsTab]/index.js | 58 +++++++++--------- .../select-option-group-label.json} | 0 .../select-option-group.json} | 0 .../select-option-indicator.json} | 0 .../select-option.json} | 0 .../api-docs/select-root/select-root.json | 4 +- .../use-select-option.json} | 0 .../src/Select/Backdrop/SelectBackdrop.tsx | 13 ++-- .../src/Select/Item/SelectItemContext.ts | 15 ----- .../SelectOption.test.tsx} | 0 .../SelectOption.tsx} | 36 +++++------ .../src/Select/Option/SelectOptionContext.ts | 15 +++++ .../useSelectOption.ts} | 6 +- .../SelectOptionGroup.test.tsx} | 2 +- .../SelectOptionGroup.tsx} | 37 ++++++++---- .../SelectOptionGroupContext.ts} | 8 +-- .../SelectOptionGroupLabel.test.tsx} | 4 +- .../SelectOptionGroupLabel.tsx} | 32 ++++++---- .../SelectOptionIndicator.test.tsx} | 8 +-- .../SelectOptionIndicator.tsx} | 42 ++++--------- .../src/Select/Popup/useSelectPopup.ts | 6 +- .../Select/Positioner/SelectPositioner.tsx | 20 +++---- .../mui-base/src/Select/Root/SelectRoot.tsx | 24 ++++---- .../src/Select/Root/SelectRootContext.ts | 2 +- .../src/Select/Root/useSelectRoot.tsx | 32 +++++++--- packages/mui-base/src/Select/index.barrel.ts | 8 +-- packages/mui-base/src/Select/index.ts | 8 +-- 37 files changed, 338 insertions(+), 242 deletions(-) rename docs/pages/base-ui/api/{select-group.json => select-option-group-label.json} (70%) rename docs/pages/base-ui/api/{select-group-label.json => select-option-group.json} (73%) rename docs/pages/base-ui/api/{select-item-indicator.json => select-option-indicator.json} (73%) rename docs/pages/base-ui/api/{select-item.json => select-option.json} (71%) create mode 100644 docs/pages/base-ui/api/use-select-option.json rename docs/translations/api-docs/{select-group-label/select-group-label.json => select-option-group-label/select-option-group-label.json} (100%) rename docs/translations/api-docs/{select-group/select-group.json => select-option-group/select-option-group.json} (100%) rename docs/translations/api-docs/{select-item-indicator/select-item-indicator.json => select-option-indicator/select-option-indicator.json} (100%) rename docs/translations/api-docs/{select-item/select-item.json => select-option/select-option.json} (100%) rename docs/translations/api-docs/{use-select-item/use-select-item.json => use-select-option/use-select-option.json} (100%) delete mode 100644 packages/mui-base/src/Select/Item/SelectItemContext.ts rename packages/mui-base/src/Select/{Item/SelectItem.test.tsx => Option/SelectOption.test.tsx} (100%) rename packages/mui-base/src/Select/{Item/SelectItem.tsx => Option/SelectOption.tsx} (87%) create mode 100644 packages/mui-base/src/Select/Option/SelectOptionContext.ts rename packages/mui-base/src/Select/{Item/useSelectItem.ts => Option/useSelectOption.ts} (93%) rename packages/mui-base/src/Select/{Group/SelectGroup.test.tsx => OptionGroup/SelectOptionGroup.test.tsx} (88%) rename packages/mui-base/src/Select/{Group/SelectGroup.tsx => OptionGroup/SelectOptionGroup.tsx} (69%) rename packages/mui-base/src/Select/{Group/SelectGroupContext.ts => OptionGroup/SelectOptionGroupContext.ts} (52%) rename packages/mui-base/src/Select/{GroupLabel/SelectGroupLabel.test.tsx => OptionGroupLabel/SelectOptionGroupLabel.test.tsx} (77%) rename packages/mui-base/src/Select/{GroupLabel/SelectGroupLabel.tsx => OptionGroupLabel/SelectOptionGroupLabel.tsx} (71%) rename packages/mui-base/src/Select/{ItemIndicator/SelectItemIndicator.test.tsx => OptionIndicator/SelectOptionIndicator.test.tsx} (69%) rename packages/mui-base/src/Select/{ItemIndicator/SelectItemIndicator.tsx => OptionIndicator/SelectOptionIndicator.tsx} (66%) diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 29555e57b..fa51c6414 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -26,23 +26,32 @@ const data = { ], }; +const entries = Object.entries(data); + export default function UnstyledSelectIntroduction() { return ( - - Trigger + + Select food... - {Object.entries(data).map(([group, items]) => ( - - {group} - {items.map((item) => ( - - {item.label} - } /> - - ))} - + + Select food... + } /> + + {entries.map(([group, items]) => ( + +
      + + {group} + {items.map((item) => ( + + {item.label} + } /> + + ))} + +
      ))}
      @@ -72,7 +81,7 @@ const SelectPopup = styled(Select.Popup)` outline: 0; `; -const SelectItem = styled(Select.Item)` +const SelectOption = styled(Select.Item)` padding: 6px 12px; outline: 0; cursor: default; @@ -92,11 +101,11 @@ const SelectItem = styled(Select.Item)` } `; -const SelectItemIndicator = styled(Select.ItemIndicator)` +const SelectOptionIndicator = styled(Select.OptionIndicator)` margin-left: 8px; `; -const SelectGroupLabel = styled(Select.GroupLabel)` +const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` font-weight: bold; padding: 4px 12px; cursor: default; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 29555e57b..fa51c6414 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -26,23 +26,32 @@ const data = { ], }; +const entries = Object.entries(data); + export default function UnstyledSelectIntroduction() { return ( - - Trigger + + Select food... - {Object.entries(data).map(([group, items]) => ( - - {group} - {items.map((item) => ( - - {item.label} - } /> - - ))} - + + Select food... + } /> + + {entries.map(([group, items]) => ( + +
      + + {group} + {items.map((item) => ( + + {item.label} + } /> + + ))} + +
      ))}
      @@ -72,7 +81,7 @@ const SelectPopup = styled(Select.Popup)` outline: 0; `; -const SelectItem = styled(Select.Item)` +const SelectOption = styled(Select.Item)` padding: 6px 12px; outline: 0; cursor: default; @@ -92,11 +101,11 @@ const SelectItem = styled(Select.Item)` } `; -const SelectItemIndicator = styled(Select.ItemIndicator)` +const SelectOptionIndicator = styled(Select.OptionIndicator)` margin-left: 8px; `; -const SelectGroupLabel = styled(Select.GroupLabel)` +const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` font-weight: bold; padding: 4px 12px; cursor: default; diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md index d66a6faf4..2972ae308 100644 --- a/docs/data/base/components/select/select.md +++ b/docs/data/base/components/select/select.md @@ -1,7 +1,7 @@ --- productId: base-ui title: React Select components and hook -components: SelectRoot, SelectTrigger, SelectBackdrop, SelectPositioner, SelectPopup, SelectItem, SelectItemIndicator, SelectGroup, SelectGroupLabel +components: SelectRoot, SelectTrigger, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- @@ -15,3 +15,60 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-sel {{"component": "modules/components/ComponentPageTabs.js"}} {{"demo": "SelectIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} + +## Installation + +Base UI components are all available as a single package. + + + +```bash npm +npm install @base_ui/react +``` + +```bash yarn +yarn add @base_ui/react +``` + +```bash pnpm +pnpm add @base_ui/react +``` + + + +Once you have the package installed, import the component. + +```ts +import * as Select from '@base_ui/react/Select'; +``` + +## Anatomy + +Selects are implemented using a collection of related components: + +- `` is a top-level component that wraps the other components. +- `` renders the trigger element that opens the select popup on click. +- `` renders a backdrop element behind the popup. +- `` renders the select popup's positioning element. +- `` renders the select popup itself. +- `` renders an option, placed inside the popup. +- `` renders an option indicator inside an option to indicate it's selected (e.g. a check icon). +- `` renders an option group, wrapping `` components. +- `` renders a label for an option group. + +```jsx + + + + + + + + + + + + + + +``` diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index de4a5ce9c..06841aada 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -229,20 +229,20 @@ module.exports = [ title: 'SelectBackdrop', }, { - pathname: '/base-ui/react-select/components-api/#select-group', - title: 'SelectGroup', + pathname: '/base-ui/react-select/components-api/#select-option', + title: 'SelectOption', }, { - pathname: '/base-ui/react-select/components-api/#select-group-label', - title: 'SelectGroupLabel', + pathname: '/base-ui/react-select/components-api/#select-option-group', + title: 'SelectOptionGroup', }, { - pathname: '/base-ui/react-select/components-api/#select-item', - title: 'SelectItem', + pathname: '/base-ui/react-select/components-api/#select-option-group-label', + title: 'SelectOptionGroupLabel', }, { - pathname: '/base-ui/react-select/components-api/#select-item-indicator', - title: 'SelectItemIndicator', + pathname: '/base-ui/react-select/components-api/#select-option-indicator', + title: 'SelectOptionIndicator', }, { pathname: '/base-ui/react-select/components-api/#select-popup', diff --git a/docs/pages/base-ui/api/select-group.json b/docs/pages/base-ui/api/select-option-group-label.json similarity index 70% rename from docs/pages/base-ui/api/select-group.json rename to docs/pages/base-ui/api/select-option-group-label.json index 5b6fcb0a7..910414040 100644 --- a/docs/pages/base-ui/api/select-group.json +++ b/docs/pages/base-ui/api/select-option-group-label.json @@ -3,16 +3,16 @@ "className": { "type": { "name": "union", "description": "func
      | string" } }, "render": { "type": { "name": "union", "description": "element
      | func" } } }, - "name": "SelectGroup", + "name": "SelectOptionGroupLabel", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectGroup = Select.Group;" + "import * as Select from '@base_ui/react/Select';\nconst SelectOptionGroupLabel = Select.OptionGroupLabel;" ], "classes": [], "spread": true, "themeDefaultProps": true, - "muiName": "SelectGroup", + "muiName": "SelectOptionGroupLabel", "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/Select/Group/SelectGroup.tsx", + "filename": "/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/select-group-label.json b/docs/pages/base-ui/api/select-option-group.json similarity index 73% rename from docs/pages/base-ui/api/select-group-label.json rename to docs/pages/base-ui/api/select-option-group.json index 8f66b91cb..9f5003716 100644 --- a/docs/pages/base-ui/api/select-group-label.json +++ b/docs/pages/base-ui/api/select-option-group.json @@ -3,16 +3,16 @@ "className": { "type": { "name": "union", "description": "func
      | string" } }, "render": { "type": { "name": "union", "description": "element
      | func" } } }, - "name": "SelectGroupLabel", + "name": "SelectOptionGroup", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectGroupLabel = Select.GroupLabel;" + "import * as Select from '@base_ui/react/Select';\nconst SelectOptionGroup = Select.OptionGroup;" ], "classes": [], "spread": true, "themeDefaultProps": true, - "muiName": "SelectGroupLabel", + "muiName": "SelectOptionGroup", "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx", + "filename": "/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/select-item-indicator.json b/docs/pages/base-ui/api/select-option-indicator.json similarity index 73% rename from docs/pages/base-ui/api/select-item-indicator.json rename to docs/pages/base-ui/api/select-option-indicator.json index 18b9f2401..f1b8105ba 100644 --- a/docs/pages/base-ui/api/select-item-indicator.json +++ b/docs/pages/base-ui/api/select-option-indicator.json @@ -4,16 +4,16 @@ "keepMounted": { "type": { "name": "bool" }, "default": "false" }, "render": { "type": { "name": "union", "description": "element
      | func" } } }, - "name": "SelectItemIndicator", + "name": "SelectOptionIndicator", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectItemIndicator = Select.ItemIndicator;" + "import * as Select from '@base_ui/react/Select';\nconst SelectOptionIndicator = Select.OptionIndicator;" ], "classes": [], "spread": true, "themeDefaultProps": true, - "muiName": "SelectItemIndicator", + "muiName": "SelectOptionIndicator", "forwardsRefTo": "HTMLSpanElement", - "filename": "/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx", + "filename": "/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/select-item.json b/docs/pages/base-ui/api/select-option.json similarity index 71% rename from docs/pages/base-ui/api/select-item.json rename to docs/pages/base-ui/api/select-option.json index 238bc0deb..a4fa4f17a 100644 --- a/docs/pages/base-ui/api/select-item.json +++ b/docs/pages/base-ui/api/select-option.json @@ -7,14 +7,16 @@ "label": { "type": { "name": "string" } }, "onClick": { "type": { "name": "func" } } }, - "name": "SelectItem", - "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectItem = Select.Item;"], + "name": "SelectOption", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectOption = Select.Option;" + ], "classes": [], "spread": true, "themeDefaultProps": true, - "muiName": "SelectItem", + "muiName": "SelectOption", "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/Select/Item/SelectItem.tsx", + "filename": "/packages/mui-base/src/Select/Option/SelectOption.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/select-root.json b/docs/pages/base-ui/api/select-root.json index 9d2b1bfe1..9ec7b965b 100644 --- a/docs/pages/base-ui/api/select-root.json +++ b/docs/pages/base-ui/api/select-root.json @@ -1,9 +1,6 @@ { "props": { - "alignMethod": { - "type": { "name": "enum", "description": "'selected-item'
      | 'trigger'" }, - "default": "'selected-item'" - }, + "alignToItem": { "type": { "name": "bool" }, "default": "true" }, "animated": { "type": { "name": "bool" }, "default": "true" }, "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, "defaultValue": { "type": { "name": "string" } }, diff --git a/docs/pages/base-ui/api/use-select-option.json b/docs/pages/base-ui/api/use-select-option.json new file mode 100644 index 000000000..b009baea3 --- /dev/null +++ b/docs/pages/base-ui/api/use-select-option.json @@ -0,0 +1,8 @@ +{ + "parameters": {}, + "returnValue": {}, + "name": "useSelectOption", + "filename": "/packages/mui-base/src/Select/Option/useSelectOption.ts", + "imports": ["import { useSelectOption } from '@base_ui/react/Select';"], + "demos": "
        " +} diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js index 9cd15c9c6..3ccb5bac1 100644 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -4,10 +4,10 @@ import AppFrame from 'docs/src/modules/components/AppFrame'; import * as pageProps from 'docs-base/data/base/components/select/select.md?@mui/markdown'; import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; import SelectBackdropApiJsonPageContent from '../../api/select-backdrop.json'; -import SelectGroupApiJsonPageContent from '../../api/select-group.json'; -import SelectGroupLabelApiJsonPageContent from '../../api/select-group-label.json'; -import SelectItemApiJsonPageContent from '../../api/select-item.json'; -import SelectItemIndicatorApiJsonPageContent from '../../api/select-item-indicator.json'; +import SelectOptionApiJsonPageContent from '../../api/select-option.json'; +import SelectOptionGroupApiJsonPageContent from '../../api/select-option-group.json'; +import SelectOptionGroupLabelApiJsonPageContent from '../../api/select-option-group-label.json'; +import SelectOptionIndicatorApiJsonPageContent from '../../api/select-option-indicator.json'; import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; import SelectPositionerApiJsonPageContent from '../../api/select-positioner.json'; import SelectRootApiJsonPageContent from '../../api/select-root.json'; @@ -37,33 +37,35 @@ export const getStaticProps = () => { ); const SelectBackdropApiDescriptions = mapApiPageTranslations(SelectBackdropApiReq); - const SelectGroupApiReq = require.context( - 'docs-base/translations/api-docs/select-group', + const SelectOptionApiReq = require.context( + 'docs-base/translations/api-docs/select-option', false, - /\.\/select-group.*.json$/, + /\.\/select-option.*.json$/, ); - const SelectGroupApiDescriptions = mapApiPageTranslations(SelectGroupApiReq); + const SelectOptionApiDescriptions = mapApiPageTranslations(SelectOptionApiReq); - const SelectGroupLabelApiReq = require.context( - 'docs-base/translations/api-docs/select-group-label', + const SelectOptionGroupApiReq = require.context( + 'docs-base/translations/api-docs/select-option-group', false, - /\.\/select-group-label.*.json$/, + /\.\/select-option-group.*.json$/, ); - const SelectGroupLabelApiDescriptions = mapApiPageTranslations(SelectGroupLabelApiReq); + const SelectOptionGroupApiDescriptions = mapApiPageTranslations(SelectOptionGroupApiReq); - const SelectItemApiReq = require.context( - 'docs-base/translations/api-docs/select-item', + const SelectOptionGroupLabelApiReq = require.context( + 'docs-base/translations/api-docs/select-option-group-label', false, - /\.\/select-item.*.json$/, + /\.\/select-option-group-label.*.json$/, + ); + const SelectOptionGroupLabelApiDescriptions = mapApiPageTranslations( + SelectOptionGroupLabelApiReq, ); - const SelectItemApiDescriptions = mapApiPageTranslations(SelectItemApiReq); - const SelectItemIndicatorApiReq = require.context( - 'docs-base/translations/api-docs/select-item-indicator', + const SelectOptionIndicatorApiReq = require.context( + 'docs-base/translations/api-docs/select-option-indicator', false, - /\.\/select-item-indicator.*.json$/, + /\.\/select-option-indicator.*.json$/, ); - const SelectItemIndicatorApiDescriptions = mapApiPageTranslations(SelectItemIndicatorApiReq); + const SelectOptionIndicatorApiDescriptions = mapApiPageTranslations(SelectOptionIndicatorApiReq); const SelectPopupApiReq = require.context( 'docs-base/translations/api-docs/select-popup', @@ -97,10 +99,10 @@ export const getStaticProps = () => { props: { componentsApiDescriptions: { SelectBackdrop: SelectBackdropApiDescriptions, - SelectGroup: SelectGroupApiDescriptions, - SelectGroupLabel: SelectGroupLabelApiDescriptions, - SelectItem: SelectItemApiDescriptions, - SelectItemIndicator: SelectItemIndicatorApiDescriptions, + SelectOption: SelectOptionApiDescriptions, + SelectOptionGroup: SelectOptionGroupApiDescriptions, + SelectOptionGroupLabel: SelectOptionGroupLabelApiDescriptions, + SelectOptionIndicator: SelectOptionIndicatorApiDescriptions, SelectPopup: SelectPopupApiDescriptions, SelectPositioner: SelectPositionerApiDescriptions, SelectRoot: SelectRootApiDescriptions, @@ -108,10 +110,10 @@ export const getStaticProps = () => { }, componentsApiPageContents: { SelectBackdrop: SelectBackdropApiJsonPageContent, - SelectGroup: SelectGroupApiJsonPageContent, - SelectGroupLabel: SelectGroupLabelApiJsonPageContent, - SelectItem: SelectItemApiJsonPageContent, - SelectItemIndicator: SelectItemIndicatorApiJsonPageContent, + SelectOption: SelectOptionApiJsonPageContent, + SelectOptionGroup: SelectOptionGroupApiJsonPageContent, + SelectOptionGroupLabel: SelectOptionGroupLabelApiJsonPageContent, + SelectOptionIndicator: SelectOptionIndicatorApiJsonPageContent, SelectPopup: SelectPopupApiJsonPageContent, SelectPositioner: SelectPositionerApiJsonPageContent, SelectRoot: SelectRootApiJsonPageContent, diff --git a/docs/translations/api-docs/select-group-label/select-group-label.json b/docs/translations/api-docs/select-option-group-label/select-option-group-label.json similarity index 100% rename from docs/translations/api-docs/select-group-label/select-group-label.json rename to docs/translations/api-docs/select-option-group-label/select-option-group-label.json diff --git a/docs/translations/api-docs/select-group/select-group.json b/docs/translations/api-docs/select-option-group/select-option-group.json similarity index 100% rename from docs/translations/api-docs/select-group/select-group.json rename to docs/translations/api-docs/select-option-group/select-option-group.json diff --git a/docs/translations/api-docs/select-item-indicator/select-item-indicator.json b/docs/translations/api-docs/select-option-indicator/select-option-indicator.json similarity index 100% rename from docs/translations/api-docs/select-item-indicator/select-item-indicator.json rename to docs/translations/api-docs/select-option-indicator/select-option-indicator.json diff --git a/docs/translations/api-docs/select-item/select-item.json b/docs/translations/api-docs/select-option/select-option.json similarity index 100% rename from docs/translations/api-docs/select-item/select-item.json rename to docs/translations/api-docs/select-option/select-option.json diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json index 5565e95b7..103d8a6d3 100644 --- a/docs/translations/api-docs/select-root/select-root.json +++ b/docs/translations/api-docs/select-root/select-root.json @@ -1,9 +1,7 @@ { "componentDescription": "", "propDescriptions": { - "alignMethod": { - "description": "Determines the type of alignment mode. selected-item aligns the popup so that the selected item appears over the trigger, while trigger aligns the popup using standard anchor positioning." - }, + "alignToItem": { "description": "Determines if the Select should align to the item." }, "animated": { "description": "If true, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." }, diff --git a/docs/translations/api-docs/use-select-item/use-select-item.json b/docs/translations/api-docs/use-select-option/use-select-option.json similarity index 100% rename from docs/translations/api-docs/use-select-item/use-select-item.json rename to docs/translations/api-docs/use-select-option/use-select-option.json diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx index fac720dd1..eea9f4184 100644 --- a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx @@ -9,6 +9,10 @@ import { useForkRef } from '../../utils/useForkRef'; import { useScrollLock } from '../../utils/useScrollLock'; import { useSelectRootContext } from '../Root/SelectRootContext'; import { useSelectBackdrop } from './useSelectBackdrop'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; + +const customStyleHookMapping: CustomStyleHookMapping = commonStyleHooks; const SelectBackdrop = React.forwardRef(function SelectBackdrop( props: SelectBackdrop.Props, @@ -16,7 +20,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop( ) { const { className, render, keepMounted = false, container, ...otherProps } = props; - const { open, mounted, innerFallback, alignMethod, selectedIndex, backdropRef } = + const { open, mounted, innerFallback, alignToItem, selectedIndex, backdropRef } = useSelectRootContext(); const { getBackdropProps } = useSelectBackdrop(); @@ -25,11 +29,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop( const ownerState: SelectBackdrop.OwnerState = React.useMemo(() => ({ open }), [open]); - const standardMode = !( - selectedIndex !== null && - alignMethod === 'selected-item' && - !innerFallback - ); + const standardMode = !(selectedIndex !== null && alignToItem && !innerFallback); useScrollLock(!standardMode && mounted); @@ -40,6 +40,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop( ownerState, ref: mergedRef, extraProps: otherProps, + customStyleHookMapping, }); const shouldRender = keepMounted || mounted; diff --git a/packages/mui-base/src/Select/Item/SelectItemContext.ts b/packages/mui-base/src/Select/Item/SelectItemContext.ts deleted file mode 100644 index e9ce406f9..000000000 --- a/packages/mui-base/src/Select/Item/SelectItemContext.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; - -interface SelectItemContext { - selected: boolean; -} - -export const SelectItemContext = React.createContext(null); - -export function useSelectItemContext() { - const context = React.useContext(SelectItemContext); - if (context === null) { - throw new Error('Base UI: useSelectItemContext is not defined.'); - } - return context; -} diff --git a/packages/mui-base/src/Select/Item/SelectItem.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx similarity index 100% rename from packages/mui-base/src/Select/Item/SelectItem.test.tsx rename to packages/mui-base/src/Select/Option/SelectOption.test.tsx diff --git a/packages/mui-base/src/Select/Item/SelectItem.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx similarity index 87% rename from packages/mui-base/src/Select/Item/SelectItem.tsx rename to packages/mui-base/src/Select/Option/SelectOption.tsx index 0d38cfffd..df42a5641 100644 --- a/packages/mui-base/src/Select/Item/SelectItem.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -2,20 +2,20 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { UseInteractionsReturn, useListItem } from '@floating-ui/react'; -import { useSelectItem } from './useSelectItem'; +import { useSelectOption } from './useSelectOption'; import { SelectRootContext, useSelectRootContext } from '../Root/SelectRootContext'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { BaseUIComponentProps } from '../../utils/types'; import { useId } from '../../utils/useId'; import { useForkRef } from '../../utils/useForkRef'; import { useEventCallback } from '../../utils/useEventCallback'; -import { SelectItemContext } from './SelectItemContext'; +import { SelectOptionContext } from './SelectOptionContext'; import { commonStyleHooks } from '../utils/commonStyleHooks'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; -const InnerSelectItem = React.memo( - React.forwardRef(function InnerSelectItem( - props: InnerSelectItemProps, +const InnerSelectOption = React.memo( + React.forwardRef(function InnerSelectOption( + props: InnerSelectOptionProps, forwardedRef: React.ForwardedRef, ) { const { @@ -35,7 +35,7 @@ const InnerSelectItem = React.memo( ...otherProps } = props; - const { getItemProps } = useSelectItem({ + const { getItemProps } = useSelectOption({ setOpen, closeOnClick, disabled, @@ -48,7 +48,7 @@ const InnerSelectItem = React.memo( selectionRef, }); - const ownerState: SelectItem.OwnerState = React.useMemo( + const ownerState: SelectOption.OwnerState = React.useMemo( () => ({ open, disabled, highlighted, selected }), [open, disabled, highlighted, selected], ); @@ -83,10 +83,10 @@ const InnerSelectItem = React.memo( * * API: * - * - [SelectItem API](https://mui.com/base-ui/react-select/components-api/#select-item) + * - [SelectOption API](https://mui.com/base-ui/react-select/components-api/#select-item) */ -const SelectItem = React.forwardRef(function SelectItem( - props: SelectItem.Props, +const SelectOption = React.forwardRef(function SelectOption( + props: SelectOption.Props, forwardedRef: React.ForwardedRef, ) { const { id: idProp, value: valueProp, label, ...otherProps } = props; @@ -133,12 +133,12 @@ const SelectItem = React.forwardRef(function SelectItem( const contextValue = React.useMemo(() => ({ open, selected }), [open, selected]); // This wrapper component is used as a performance optimization. - // SelectItem reads the context and re-renders the actual SelectItem + // SelectOption reads the context and re-renders the actual SelectOption // only when it needs to. return ( - - + - + ); }); -interface InnerSelectItemProps extends Omit { +interface InnerSelectOptionProps extends Omit { highlighted: boolean; selected: boolean; getItemProps: UseInteractionsReturn['getItemProps']; @@ -169,7 +169,7 @@ interface InnerSelectItemProps extends Omit { open: boolean; } -namespace SelectItem { +namespace SelectOption { export interface OwnerState { disabled: boolean; highlighted: boolean; @@ -210,7 +210,7 @@ namespace SelectItem { } } -SelectItem.propTypes /* remove-proptypes */ = { +SelectOption.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -249,4 +249,4 @@ SelectItem.propTypes /* remove-proptypes */ = { value: PropTypes.string.isRequired, } as any; -export { SelectItem }; +export { SelectOption }; diff --git a/packages/mui-base/src/Select/Option/SelectOptionContext.ts b/packages/mui-base/src/Select/Option/SelectOptionContext.ts new file mode 100644 index 000000000..85f8f420a --- /dev/null +++ b/packages/mui-base/src/Select/Option/SelectOptionContext.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; + +interface SelectOptionContext { + selected: boolean; +} + +export const SelectOptionContext = React.createContext(null); + +export function useSelectOptionContext() { + const context = React.useContext(SelectOptionContext); + if (context === null) { + throw new Error('Base UI: useSelectOptionContext is not defined.'); + } + return context; +} diff --git a/packages/mui-base/src/Select/Item/useSelectItem.ts b/packages/mui-base/src/Select/Option/useSelectOption.ts similarity index 93% rename from packages/mui-base/src/Select/Item/useSelectItem.ts rename to packages/mui-base/src/Select/Option/useSelectOption.ts index 621a73896..e80d68804 100644 --- a/packages/mui-base/src/Select/Item/useSelectItem.ts +++ b/packages/mui-base/src/Select/Option/useSelectOption.ts @@ -10,9 +10,9 @@ import { useEventCallback } from '../../utils/useEventCallback'; * * API: * - * - [useSelectItem API](https://mui.com/base-ui/api/use-select-item/) + * - [useSelectOption API](https://mui.com/base-ui/api/use-select-option/) */ -export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.ReturnValue { +export function useSelectOption(params: useSelectOption.Parameters): useSelectOption.ReturnValue { const { disabled = false, highlighted, @@ -89,7 +89,7 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R ); } -export namespace useSelectItem { +export namespace useSelectOption { export interface Parameters { /** * If `true`, the select will close when the select item is clicked. diff --git a/packages/mui-base/src/Select/Group/SelectGroup.test.tsx b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx similarity index 88% rename from packages/mui-base/src/Select/Group/SelectGroup.test.tsx rename to packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx index 22416da9f..6e048805b 100644 --- a/packages/mui-base/src/Select/Group/SelectGroup.test.tsx +++ b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx @@ -5,7 +5,7 @@ import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( diff --git a/packages/mui-base/src/Select/Group/SelectGroup.tsx b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx similarity index 69% rename from packages/mui-base/src/Select/Group/SelectGroup.tsx rename to packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx index 1e0fd82e2..c0ecfbdf1 100644 --- a/packages/mui-base/src/Select/Group/SelectGroup.tsx +++ b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx @@ -1,21 +1,30 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { SelectGroupContext } from './SelectGroupContext'; +import { SelectOptionGroupContext } from './SelectOptionGroupContext'; +import { useSelectRootContext } from '../Root/SelectRootContext'; -const SelectGroup = React.forwardRef(function SelectGroup( - props: SelectGroup.Props, +const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( + props: SelectOptionGroup.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, ...otherProps } = props; + const { open } = useSelectRootContext(); + const [labelId, setLabelId] = React.useState(); - const ownerState: SelectGroup.OwnerState = React.useMemo(() => ({}), []); + const ownerState: SelectOptionGroup.OwnerState = React.useMemo( + () => ({ + open, + }), + [open], + ); - const getSelectGroupProps = React.useCallback( + const getSelectOptionGroupProps = React.useCallback( (externalProps = {}) => mergeReactProps(externalProps, { role: 'group', @@ -24,7 +33,7 @@ const SelectGroup = React.forwardRef(function SelectGroup( [labelId], ); - const contextValue: SelectGroupContext = React.useMemo( + const contextValue: SelectOptionGroupContext = React.useMemo( () => ({ labelId, setLabelId, @@ -33,7 +42,7 @@ const SelectGroup = React.forwardRef(function SelectGroup( ); const { renderElement } = useComponentRenderer({ - propGetter: getSelectGroupProps, + propGetter: getSelectOptionGroupProps, render: render ?? 'div', ref: forwardedRef, ownerState, @@ -42,13 +51,13 @@ const SelectGroup = React.forwardRef(function SelectGroup( }); return ( - + {renderElement()} - + ); }); -SelectGroup.propTypes /* remove-proptypes */ = { +SelectOptionGroup.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -67,9 +76,11 @@ SelectGroup.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -namespace SelectGroup { - export interface OwnerState {} +namespace SelectOptionGroup { + export interface OwnerState { + open: boolean; + } export interface Props extends BaseUIComponentProps<'div', OwnerState> {} } -export { SelectGroup }; +export { SelectOptionGroup }; diff --git a/packages/mui-base/src/Select/Group/SelectGroupContext.ts b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroupContext.ts similarity index 52% rename from packages/mui-base/src/Select/Group/SelectGroupContext.ts rename to packages/mui-base/src/Select/OptionGroup/SelectOptionGroupContext.ts index 1cd08b6f8..47b885bcf 100644 --- a/packages/mui-base/src/Select/Group/SelectGroupContext.ts +++ b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroupContext.ts @@ -1,14 +1,14 @@ import * as React from 'react'; -export interface SelectGroupContext { +export interface SelectOptionGroupContext { labelId: string | undefined; setLabelId: React.Dispatch>; } -export const SelectGroupContext = React.createContext(null); +export const SelectOptionGroupContext = React.createContext(null); -export function useSelectGroupContext() { - const context = React.useContext(SelectGroupContext); +export function useSelectOptionGroupContext() { + const context = React.useContext(SelectOptionGroupContext); if (context === null) { throw new Error('Base UI: must be used within a '); } diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx b/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.test.tsx similarity index 77% rename from packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx rename to packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.test.tsx index 07f42abc6..fc421d985 100644 --- a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx +++ b/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.test.tsx @@ -5,12 +5,12 @@ import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( - {node} + {node} , ); }, diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx b/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx similarity index 71% rename from packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx rename to packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx index 60357d20a..b3a3d9190 100644 --- a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx +++ b/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx @@ -1,21 +1,29 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useId } from '../../utils/useId'; -import { useSelectGroupContext } from '../Group/SelectGroupContext'; +import { useSelectOptionGroupContext } from '../OptionGroup/SelectOptionGroupContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useSelectRootContext } from '../Root/SelectRootContext'; -const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( - props: SelectGroupLabel.Props, +const SelectOptionGroupLabel = React.forwardRef(function SelectOptionGroupLabel( + props: SelectOptionGroupLabel.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, id: idProp, ...otherProps } = props; - const { setLabelId } = useSelectGroupContext(); + const { open } = useSelectRootContext(); + const { setLabelId } = useSelectOptionGroupContext(); - const ownerState: SelectGroupLabel.OwnerState = React.useMemo(() => ({}), []); + const ownerState: SelectOptionGroupLabel.OwnerState = React.useMemo( + () => ({ + open, + }), + [open], + ); const id = useId(idProp); @@ -23,7 +31,7 @@ const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( setLabelId(id); }, [id, setLabelId]); - const getSelectGroupLabelProps = React.useCallback( + const getSelectOptionGroupLabelProps = React.useCallback( (externalProps = {}) => mergeReactProps(externalProps, { id, @@ -32,7 +40,7 @@ const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( ); const { renderElement } = useComponentRenderer({ - propGetter: getSelectGroupLabelProps, + propGetter: getSelectOptionGroupLabelProps, render: render ?? 'div', ref: forwardedRef, ownerState, @@ -43,7 +51,7 @@ const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( return renderElement(); }); -SelectGroupLabel.propTypes /* remove-proptypes */ = { +SelectOptionGroupLabel.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -66,9 +74,11 @@ SelectGroupLabel.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -namespace SelectGroupLabel { - export interface OwnerState {} +namespace SelectOptionGroupLabel { + export interface OwnerState { + open: boolean; + } export interface Props extends BaseUIComponentProps<'div', OwnerState> {} } -export { SelectGroupLabel }; +export { SelectOptionGroupLabel }; diff --git a/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx similarity index 69% rename from packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx rename to packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx index 1f48ef7c6..235bdb8c9 100644 --- a/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.test.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; -import { SelectItemContext } from '../Item/SelectItemContext'; +import { SelectOptionContext } from '../Option/SelectOptionContext'; const selectItemContextValue = { open: true, @@ -11,15 +11,15 @@ const selectItemContextValue = { describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLSpanElement, render(node) { return render( - + {node} - + , ); diff --git a/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx similarity index 66% rename from packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx rename to packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx index b4b0a9a26..017a933c9 100644 --- a/packages/mui-base/src/Select/ItemIndicator/SelectItemIndicator.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx @@ -1,45 +1,31 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import type { Side } from '@floating-ui/react'; import type { BaseUIComponentProps } from '../../utils/types'; import { useSelectRootContext } from '../Root/SelectRootContext'; -import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; import { commonStyleHooks } from '../utils/commonStyleHooks'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; -import { useSelectItemContext } from '../Item/SelectItemContext'; +import { useSelectOptionContext } from '../Option/SelectOptionContext'; -const customStyleHookMapping: CustomStyleHookMapping = { - ...commonStyleHooks, - entering(value) { - return value ? { 'data-select-entering': '' } : null; - }, - exiting(value) { - return value ? { 'data-select-exiting': '' } : null; - }, -}; +const customStyleHookMapping: CustomStyleHookMapping = + commonStyleHooks; -const SelectItemIndicator = React.forwardRef(function SelectItemIndicator( - props: SelectItemIndicator.Props, +const SelectOptionIndicator = React.forwardRef(function SelectOptionIndicator( + props: SelectOptionIndicator.Props, forwardedRef: React.ForwardedRef, ) { const { render, className, keepMounted = false, ...otherProps } = props; - const { open, transitionStatus } = useSelectRootContext(); - const { side, alignment } = useSelectPositionerContext(); - const { selected } = useSelectItemContext(); + const { open } = useSelectRootContext(); + const { selected } = useSelectOptionContext(); - const ownerState: SelectItemIndicator.OwnerState = React.useMemo( + const ownerState: SelectOptionIndicator.OwnerState = React.useMemo( () => ({ - entering: transitionStatus === 'entering', - exiting: transitionStatus === 'exiting', - side, - alignment, open, selected, }), - [transitionStatus, side, alignment, open, selected], + [open, selected], ); const { renderElement } = useComponentRenderer({ @@ -59,7 +45,7 @@ const SelectItemIndicator = React.forwardRef(function SelectItemIndicator( return renderElement(); }); -namespace SelectItemIndicator { +namespace SelectOptionIndicator { export interface Props extends BaseUIComponentProps<'span', OwnerState> { children?: React.ReactNode; /** @@ -71,16 +57,12 @@ namespace SelectItemIndicator { } export interface OwnerState { - entering: boolean; - exiting: boolean; - side: Side; - alignment: 'start' | 'end' | 'center'; open: boolean; selected: boolean; } } -SelectItemIndicator.propTypes /* remove-proptypes */ = { +SelectOptionIndicator.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -105,4 +87,4 @@ SelectItemIndicator.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -export { SelectItemIndicator }; +export { SelectOptionIndicator }; diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 709eef241..2b0c9a622 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -12,7 +12,7 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; export function useSelectPopup(): useSelectPopup.ReturnValue { const { getPopupProps: getRootPopupProps, - alignMethod, + alignToItem, selectedIndex, innerFallback, } = useSelectRootContext(); @@ -26,7 +26,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { // must be relative to the element. position: 'relative', overflowY: 'auto', - ...(alignMethod === 'selected-item' && + ...(alignToItem && hasSelectedIndex && !innerFallback && { scrollbarWidth: 'none', @@ -34,7 +34,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { }, }); }, - [getRootPopupProps, alignMethod, hasSelectedIndex, innerFallback], + [getRootPopupProps, alignToItem, hasSelectedIndex, innerFallback], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index ea988b76f..7b9197b54 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -21,19 +21,19 @@ import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useId } from '../../utils/useId'; -function findSelectItems(root: React.ReactElement) { - const selectItems: React.ReactElement[] = []; +function findSelectOptions(root: React.ReactElement) { + const SelectOptions: React.ReactElement[] = []; React.Children.forEach(root.props?.children, (child) => { if (React.isValidElement(child)) { const childProps = child.props as any; if (childProps?.value != null) { - selectItems.push(child); + SelectOptions.push(child); } else if (childProps?.children) { - selectItems.push(...findSelectItems(child)); + SelectOptions.push(...findSelectOptions(child)); } } }); - return selectItems; + return SelectOptions; } /** @@ -81,7 +81,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( popupRef, overflowRef, innerOffset, - alignMethod, + alignToItem, innerFallback, setInnerFallback, selectedIndex, @@ -121,7 +121,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( allowAxisFlip: false, innerFallback, inner: - alignMethod === 'selected-item' && selectedIndex !== null + alignToItem && selectedIndex !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ @@ -185,8 +185,8 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( }); const positionerElement = renderElement(); - const selectItems = findSelectItems(positionerElement); - const mountedItemsElement = keepMounted ? null : ; + const SelectOptions = findSelectOptions(positionerElement); + const mountedItemsElement = keepMounted ? null : ; const nativeSelectElement = ( to the trigger element. + triggerElement?.focus(); + }, + onChange(event: React.ChangeEvent) { + // Workaround for https://github.com/facebook/react/issues/9023 + if (event.nativeEvent.defaultPrevented) { + return; + } + + const nextValue = event.target.value; + + setDirty(nextValue !== validityData.initialValue); + setValue?.(nextValue, event.nativeEvent); + }, + })} > - {SelectOptions.map((item) => ( + {options.map((item) => ( // eslint-disable-next-line jsx-a11y/control-has-associated-label + +
        ``` diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 2d93a0d96..fd9e74f2c 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -256,6 +256,10 @@ module.exports = [ pathname: '/base-ui/react-select/components-api/#select-root', title: 'SelectRoot', }, + { + pathname: '/base-ui/react-select/components-api/#select-scroll-arrow', + title: 'SelectScrollArrow', + }, { pathname: '/base-ui/react-select/components-api/#select-trigger', title: 'SelectTrigger', diff --git a/docs/pages/base-ui/api/select-scroll-arrow.json b/docs/pages/base-ui/api/select-scroll-arrow.json new file mode 100644 index 000000000..1105fd689 --- /dev/null +++ b/docs/pages/base-ui/api/select-scroll-arrow.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
        | string" } }, + "render": { "type": { "name": "union", "description": "element
        | func" } } + }, + "name": "SelectScrollArrow", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectScrollArrow = Select.ScrollArrow;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectScrollArrow", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js index 9f1aaa4f4..de6a6039c 100644 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -11,6 +11,7 @@ import SelectOptionIndicatorApiJsonPageContent from '../../api/select-option-ind import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; import SelectPositionerApiJsonPageContent from '../../api/select-positioner.json'; import SelectRootApiJsonPageContent from '../../api/select-root.json'; +import SelectScrollArrowApiJsonPageContent from '../../api/select-scroll-arrow.json'; import SelectTriggerApiJsonPageContent from '../../api/select-trigger.json'; import SelectValueApiJsonPageContent from '../../api/select-value.json'; @@ -89,6 +90,13 @@ export const getStaticProps = () => { ); const SelectRootApiDescriptions = mapApiPageTranslations(SelectRootApiReq); + const SelectScrollArrowApiReq = require.context( + 'docs-base/translations/api-docs/select-scroll-arrow', + false, + /\.\/select-scroll-arrow.*.json$/, + ); + const SelectScrollArrowApiDescriptions = mapApiPageTranslations(SelectScrollArrowApiReq); + const SelectTriggerApiReq = require.context( 'docs-base/translations/api-docs/select-trigger', false, @@ -114,6 +122,7 @@ export const getStaticProps = () => { SelectPopup: SelectPopupApiDescriptions, SelectPositioner: SelectPositionerApiDescriptions, SelectRoot: SelectRootApiDescriptions, + SelectScrollArrow: SelectScrollArrowApiDescriptions, SelectTrigger: SelectTriggerApiDescriptions, SelectValue: SelectValueApiDescriptions, }, @@ -126,6 +135,7 @@ export const getStaticProps = () => { SelectPopup: SelectPopupApiJsonPageContent, SelectPositioner: SelectPositionerApiJsonPageContent, SelectRoot: SelectRootApiJsonPageContent, + SelectScrollArrow: SelectScrollArrowApiJsonPageContent, SelectTrigger: SelectTriggerApiJsonPageContent, SelectValue: SelectValueApiJsonPageContent, }, diff --git a/docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json b/docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 3cf76e61e..08787b063 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -178,6 +178,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const contextValue: SelectPositionerContext = React.useMemo( () => ({ + isPositioned: positioner.isPositioned, side: positioner.side, alignment: positioner.alignment, arrowRef: positioner.arrowRef, @@ -186,6 +187,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( floatingContext: positioner.floatingContext, }), [ + positioner.isPositioned, positioner.side, positioner.alignment, positioner.arrowRef, diff --git a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts index 7c78958f5..627ce4cc1 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts +++ b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts @@ -14,6 +14,7 @@ export interface SelectPositionerContext { arrowRef: React.MutableRefObject; arrowUncentered: boolean; arrowStyles: React.CSSProperties; + isPositioned: boolean; } export const SelectPositionerContext = React.createContext(null); diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 21cb389be..aff81ce94 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -31,6 +31,7 @@ export function useSelectPositioner( renderedSide, renderedAlignment, positionerContext: floatingContext, + isPositioned, } = useAnchorPositioning({ ...params, trackAnchor: !(params.inner && !params.innerFallback), @@ -67,6 +68,7 @@ export function useSelectPositioner( side: renderedSide, alignment: renderedAlignment, floatingContext, + isPositioned, }), [ getPositionerProps, @@ -76,6 +78,7 @@ export function useSelectPositioner( renderedSide, renderedAlignment, floatingContext, + isPositioned, ], ); } @@ -220,5 +223,9 @@ export namespace useSelectPositioner { * The floating context. */ floatingContext: FloatingContext; + /** + * Whether the Select popup has been positioned. + */ + isPositioned: boolean; } } diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 5f06c2f8f..84ab88442 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -134,10 +134,14 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R }); ReactDOM.flushSync(() => { - setInnerOffset(0); - setInnerFallback(false); setMounted(false); }); + + // Ensure these aren't flushed synchronously so they occur after the popup has been unmounted. + // This ensures the position calculation won't run again (which otherwise leaves + // `isPositioned` set to `true` incorrectly once closed). + setInnerOffset(0); + setInnerFallback(false); } if (!nextOpen) { @@ -253,6 +257,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R selectionRef, overflowRef, innerOffset, + setInnerOffset, innerFallback, setInnerFallback, touchModality, @@ -361,6 +366,7 @@ export namespace useSelectRoot { select: boolean; }>; innerOffset: number; + setInnerOffset: React.Dispatch>; overflowRef: React.MutableRefObject; backdropRef: React.RefObject; innerFallback: boolean; diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.test.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.test.tsx new file mode 100644 index 000000000..ee3869d3a --- /dev/null +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx new file mode 100644 index 000000000..f71b2d69d --- /dev/null +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -0,0 +1,185 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; +import { useEventCallback } from '../../utils/useEventCallback'; + +const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( + props: SelectScrollArrow.Props, + forwardedRef: React.ForwardedRef, +) { + const { render, className, direction, ...otherProps } = props; + + const { innerOffset, setInnerOffset, innerFallback, alignToItem, popupRef, touchModality } = + useSelectRootContext(); + const { isPositioned } = useSelectPositionerContext(); + + const ownerState: SelectScrollArrow.OwnerState = React.useMemo( + () => ({ + direction, + }), + [direction], + ); + + const inert = !(!touchModality && alignToItem && !innerFallback); + + const frameRef = React.useRef(-1); + + const [rendered, setRendered] = React.useState(false); + + const getScrollArrowProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'div'>(externalProps, { + 'aria-hidden': true, + style: { + position: 'absolute', + zIndex: 2147483647, // max z-index + ...(direction === 'up' && { top: 0 }), + ...(direction === 'down' && { bottom: 0 }), + }, + onMouseEnter() { + if (inert) { + return; + } + + let prevNow = Date.now(); + + function handleFrame() { + if (!popupRef.current) { + return; + } + + const currentNow = Date.now(); + const msElapsed = currentNow - prevNow; + prevNow = currentNow; + + const pixelsToScroll = Math.min( + msElapsed / 2, + popupRef.current.scrollHeight - popupRef.current.clientHeight, + ); + + const isScrolledToTop = popupRef.current.scrollTop === 0; + const isScrolledToBottom = + Math.ceil(popupRef.current.scrollTop + popupRef.current.clientHeight) >= + popupRef.current.scrollHeight; + + if (msElapsed > 0) { + if (direction === 'up') { + setRendered(!isScrolledToTop); + } else if (direction === 'down') { + setRendered(!isScrolledToBottom); + } + + if ( + (direction === 'up' && isScrolledToTop) || + (direction === 'down' && isScrolledToBottom) + ) { + return; + } + } + + const scrollDirection = direction === 'up' ? -1 : 1; + setInnerOffset((o) => o + scrollDirection * pixelsToScroll); + frameRef.current = requestAnimationFrame(handleFrame); + } + + requestAnimationFrame(handleFrame); + }, + onMouseLeave() { + cancelAnimationFrame(frameRef.current); + }, + }), + [direction, popupRef, setInnerOffset, inert], + ); + + const handleScrollArrowRendered = useEventCallback(() => { + const popupElement = popupRef.current; + if (!popupElement) { + return; + } + + if (direction === 'up') { + setRendered(popupElement.scrollTop > 1); + } else if (direction === 'down') { + const isScrolledToBottom = + Math.ceil(popupElement.scrollTop + popupElement.clientHeight) >= + popupElement.scrollHeight - 1; + + setRendered(!isScrolledToBottom); + } + }); + + React.useEffect(() => { + const popupElement = popupRef.current; + if (!popupElement || inert) { + return undefined; + } + + popupElement.addEventListener('wheel', handleScrollArrowRendered); + + return () => { + popupElement.removeEventListener('wheel', handleScrollArrowRendered); + }; + }, [inert, popupRef, direction, handleScrollArrowRendered]); + + useEnhancedEffect(() => { + if (!isPositioned || inert) { + return; + } + + handleScrollArrowRendered(); + }, [isPositioned, innerOffset, inert, handleScrollArrowRendered]); + + const { renderElement } = useComponentRenderer({ + propGetter: getScrollArrowProps, + ref: forwardedRef, + render: render ?? 'div', + className, + ownerState, + extraProps: otherProps, + }); + + if (!rendered) { + return null; + } + + return renderElement(); +}); + +SelectScrollArrow.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * @ignore + */ + direction: PropTypes.oneOf(['down', 'up']).isRequired, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +namespace SelectScrollArrow { + export interface OwnerState { + direction: 'up' | 'down'; + } + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + direction: 'up' | 'down'; + } +} + +export { SelectScrollArrow }; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index 7a7a580a1..f2d0a9705 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -8,3 +8,4 @@ export { SelectOptionIndicator } from './OptionIndicator/SelectOptionIndicator'; export { SelectOptionGroup } from './OptionGroup/SelectOptionGroup'; export { SelectOptionGroupLabel } from './OptionGroupLabel/SelectOptionGroupLabel'; export { SelectValue } from './Value/SelectValue'; +export { SelectScrollArrow } from './ScrollArrow/SelectScrollArrow'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 55c354ba6..c3d9f9a99 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -8,3 +8,4 @@ export { SelectOptionIndicator as OptionIndicator } from './OptionIndicator/Sele export { SelectOptionGroup as OptionGroup } from './OptionGroup/SelectOptionGroup'; export { SelectOptionGroupLabel as OptionGroupLabel } from './OptionGroupLabel/SelectOptionGroupLabel'; export { SelectValue as Value } from './Value/SelectValue'; +export { SelectScrollArrow as ScrollArrow } from './ScrollArrow/SelectScrollArrow'; diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index c54ab4c56..042e672e4 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -46,6 +46,7 @@ interface UseAnchorPositioningParameters { arrowPadding?: number; floatingRootContext?: FloatingRootContext; mounted?: boolean; + open?: boolean; trackAnchor?: boolean; nodeId?: string; inner?: Middleware; @@ -63,6 +64,7 @@ interface UseAnchorPositioningReturnValue { hidden: boolean; refs: ReturnType['refs']; positionerContext: FloatingContext; + isPositioned: boolean; } /** @@ -91,6 +93,7 @@ export function useAnchorPositioning( mounted = true, trackAnchor = true, allowAxisFlip = true, + open, nodeId, inner: innerMiddleware, innerFallback, @@ -206,8 +209,10 @@ export function useAnchorPositioning( update, placement: renderedPlacement, context: positionerContext, + isPositioned, } = useFloating({ rootContext: floatingRootContext, + open, placement, middleware, strategy: positionStrategy, @@ -290,6 +295,7 @@ export function useAnchorPositioning( hidden, refs, positionerContext, + isPositioned, }), [ positionerStyles, @@ -301,6 +307,7 @@ export function useAnchorPositioning( hidden, refs, positionerContext, + isPositioned, ], ); } From 141a22da35142bf956fa71d17c2d5e74db411d31 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 28 Aug 2024 17:49:53 +1000 Subject: [PATCH 18/94] Reorder namespace type --- .../Select/ScrollArrow/SelectScrollArrow.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index f71b2d69d..ea3455b7d 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -150,6 +150,15 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( return renderElement(); }); +namespace SelectScrollArrow { + export interface OwnerState { + direction: 'up' | 'down'; + } + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + direction: 'up' | 'down'; + } +} + SelectScrollArrow.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -173,13 +182,4 @@ SelectScrollArrow.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -namespace SelectScrollArrow { - export interface OwnerState { - direction: 'up' | 'down'; - } - export interface Props extends BaseUIComponentProps<'div', OwnerState> { - direction: 'up' | 'down'; - } -} - export { SelectScrollArrow }; From 520aab4733c94647ea57462a8ab44738ef6c886f Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 28 Aug 2024 18:05:17 +1000 Subject: [PATCH 19/94] Avoid issues --- .../src/Select/Positioner/SelectPositioner.tsx | 6 +++--- .../src/Select/Positioner/useSelectPositioner.tsx | 1 + packages/mui-base/src/Select/Root/useSelectRoot.tsx | 13 ------------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 08787b063..227f64979 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -144,14 +144,14 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( allowAxisFlip: false, innerFallback, inner: - alignToItem && selectedIndex !== null + alignToItem && selectedIndexOnMount !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ boundary: collisionBoundary, padding: collisionPadding, listRef: elementsRef, - index: selectedIndexOnMount ?? 0, + index: selectedIndexOnMount, scrollRef: popupRef, offset: innerOffset, onFallbackChange(fallbackValue) { @@ -160,7 +160,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( popupRef.current.style.maxHeight = ''; } }, - minItemsVisible: touchModality ? 6 : 4, + minItemsVisible: touchModality ? 8 : 4, referenceOverflowThreshold: 20, overflowRef, }) diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index aff81ce94..5bb664cf9 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -11,6 +11,7 @@ import type { import type { GenericHTMLProps } from '../../utils/types'; import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; import { mergeReactProps } from '../../utils/mergeReactProps'; + /** * * API: diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 84ab88442..eed75a77d 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -119,20 +119,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R onOpenChange?.(nextOpen, event); setOpenUnwrapped(nextOpen); - const scrollTop = popupRef.current?.scrollTop ?? 0; - function handleUnmounted() { - // Workaround issue where the `.scrollTop` by the `inner` middleware is incorrectly changed - // when a new option has been selected and the scroll was changed. Only visible if the popup - // is animating out. - requestAnimationFrame(() => { - requestAnimationFrame(() => { - if (popupRef.current) { - popupRef.current.scrollTop = scrollTop; - } - }); - }); - ReactDOM.flushSync(() => { setMounted(false); }); From 364a134277eadc00b2e6d5e56f55951c5ee389d6 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 30 Aug 2024 16:29:05 +1000 Subject: [PATCH 20/94] Update --- .../select/SelectIntroduction/system/index.js | 23 +++++++++++++++--- .../SelectIntroduction/system/index.tsx | 23 +++++++++++++++--- .../api-docs/select-option/select-option.json | 12 +++++----- .../src/Select/Option/SelectOption.tsx | 24 +++++++++---------- .../src/Select/Option/useSelectOption.ts | 9 +++++-- .../Select/Positioner/SelectPositioner.tsx | 11 +++++---- .../src/Select/Root/useSelectRoot.tsx | 3 --- .../Select/ScrollArrow/SelectScrollArrow.tsx | 19 +++++++++------ 8 files changed, 83 insertions(+), 41 deletions(-) diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 16d2713c7..9b82dc871 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -66,6 +66,7 @@ export default function UnstyledSelectIntroduction() { + @@ -82,11 +83,15 @@ export default function UnstyledSelectIntroduction() { {entries.map(([group, items]) => ( -
        +
        {group} {items.map((item) => ( - + {item.label} } @@ -108,6 +113,9 @@ export default function UnstyledSelectIntroduction() { const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; padding: 6px 12px; border-radius: 5px; background-color: black; @@ -123,6 +131,10 @@ const SelectTrigger = styled(Select.Trigger)` } `; +const SelectDropdownArrow = styled(ArrowDropDown)` + margin-right: -6px; +`; + const SelectPopup = styled(Select.Popup)` background-color: white; padding: 4px; @@ -145,6 +157,11 @@ const SelectOption = styled(Select.Option)` align-items: center; justify-content: space-between; line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } &[data-highlighted], &:focus { @@ -170,7 +187,7 @@ const SelectScrollArrow = styled(Select.ScrollArrow)` display: flex; align-items: center; justify-content: center; - height: 20px; + height: 15px; border-radius: 5px; &[data-direction='up'] { diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 16d2713c7..9b82dc871 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -66,6 +66,7 @@ export default function UnstyledSelectIntroduction() { + @@ -82,11 +83,15 @@ export default function UnstyledSelectIntroduction() { {entries.map(([group, items]) => ( -
        +
        {group} {items.map((item) => ( - + {item.label} } @@ -108,6 +113,9 @@ export default function UnstyledSelectIntroduction() { const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; padding: 6px 12px; border-radius: 5px; background-color: black; @@ -123,6 +131,10 @@ const SelectTrigger = styled(Select.Trigger)` } `; +const SelectDropdownArrow = styled(ArrowDropDown)` + margin-right: -6px; +`; + const SelectPopup = styled(Select.Popup)` background-color: white; padding: 4px; @@ -145,6 +157,11 @@ const SelectOption = styled(Select.Option)` align-items: center; justify-content: space-between; line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } &[data-highlighted], &:focus { @@ -170,7 +187,7 @@ const SelectScrollArrow = styled(Select.ScrollArrow)` display: flex; align-items: center; justify-content: center; - height: 20px; + height: 15px; border-radius: 5px; &[data-direction='up'] { diff --git a/docs/translations/api-docs/select-option/select-option.json b/docs/translations/api-docs/select-option/select-option.json index 685e50293..4cd29725c 100644 --- a/docs/translations/api-docs/select-option/select-option.json +++ b/docs/translations/api-docs/select-option/select-option.json @@ -2,15 +2,15 @@ "componentDescription": "An unstyled select item to be used within a Select.", "propDescriptions": { "closeOnClick": { - "description": "If true, the select will close when the select item is clicked." + "description": "If true, the select will close when the select option is clicked." }, - "disabled": { "description": "If true, the select item will be disabled." }, - "id": { "description": "The id of the select item." }, + "disabled": { "description": "If true, the select option will be disabled." }, + "id": { "description": "The id of the select option." }, "label": { - "description": "A text representation of the select item's content. Used for keyboard text navigation matching." + "description": "A text representation of the select option's content. Used for keyboard text navigation matching." }, - "onClick": { "description": "The click handler for the select item." }, - "value": { "description": "The value of the select item." } + "onClick": { "description": "The click handler for the select option." }, + "value": { "description": "The value of the select option." } }, "classDescriptions": {} } diff --git a/packages/mui-base/src/Select/Option/SelectOption.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx index df42a5641..f2e3aa919 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -180,29 +180,29 @@ namespace SelectOption { export interface Props extends BaseUIComponentProps<'div', OwnerState> { children?: React.ReactNode; /** - * The value of the select item. + * The value of the select option. */ value: string; /** - * The click handler for the select item. + * The click handler for the select option. */ onClick?: React.MouseEventHandler; /** - * If `true`, the select item will be disabled. + * If `true`, the select option will be disabled. * @default false */ disabled?: boolean; /** - * A text representation of the select item's content. + * A text representation of the select option's content. * Used for keyboard text navigation matching. */ label?: string; /** - * The id of the select item. + * The id of the select option. */ id?: string; /** - * If `true`, the select will close when the select item is clicked. + * If `true`, the select will close when the select option is clicked. * * @default true */ @@ -220,31 +220,31 @@ SelectOption.propTypes /* remove-proptypes */ = { */ children: PropTypes.node, /** - * If `true`, the select will close when the select item is clicked. + * If `true`, the select will close when the select option is clicked. * * @default true */ closeOnClick: PropTypes.bool, /** - * If `true`, the select item will be disabled. + * If `true`, the select option will be disabled. * @default false */ disabled: PropTypes.bool, /** - * The id of the select item. + * The id of the select option. */ id: PropTypes.string, /** - * A text representation of the select item's content. + * A text representation of the select option's content. * Used for keyboard text navigation matching. */ label: PropTypes.string, /** - * The click handler for the select item. + * The click handler for the select option. */ onClick: PropTypes.func, /** - * The value of the select item. + * The value of the select option. */ value: PropTypes.string.isRequired, } as any; diff --git a/packages/mui-base/src/Select/Option/useSelectOption.ts b/packages/mui-base/src/Select/Option/useSelectOption.ts index e39b6588a..0e65bd581 100644 --- a/packages/mui-base/src/Select/Option/useSelectOption.ts +++ b/packages/mui-base/src/Select/Option/useSelectOption.ts @@ -16,6 +16,7 @@ export function useSelectOption(params: useSelectOption.Parameters): useSelectOp const { disabled = false, highlighted, + selected, id, ref: externalRef, setOpen, @@ -42,8 +43,12 @@ export function useSelectOption(params: useSelectOption.Parameters): useSelectOp (externalProps?: GenericHTMLProps): GenericHTMLProps => { return getButtonProps( mergeReactProps<'div'>(externalProps, { + 'aria-disabled': disabled || undefined, id, tabIndex: highlighted ? 0 : -1, + style: { + pointerEvents: disabled ? 'none' : undefined, + }, onTouchStart() { selectionRef.current = { mouseUp: false, select: true }; }, @@ -78,7 +83,7 @@ export function useSelectOption(params: useSelectOption.Parameters): useSelectOp return; } - if (selectionRef.current.select) { + if (selectionRef.current.select || !selected) { commitSelection(event.nativeEvent); } @@ -87,7 +92,7 @@ export function useSelectOption(params: useSelectOption.Parameters): useSelectOp }), ); }, - [commitSelection, getButtonProps, highlighted, id, selectionRef, typingRef], + [commitSelection, disabled, getButtonProps, highlighted, id, selected, selectionRef, typingRef], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 227f64979..e6173491e 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -24,18 +24,18 @@ import { useLatestRef } from '../../utils/useLatestRef'; import { mergeReactProps } from '../../utils/mergeReactProps'; function findSelectOptions(root: React.ReactElement) { - const SelectOptions: React.ReactElement[] = []; + const options: React.ReactElement[] = []; React.Children.forEach(root.props?.children, (child) => { if (React.isValidElement(child)) { const childProps = child.props as any; if (childProps?.value != null) { - SelectOptions.push(child); + options.push(child); } else if (childProps?.children) { - SelectOptions.push(...findSelectOptions(child)); + options.push(...findSelectOptions(child)); } } }); - return SelectOptions; + return options; } /** @@ -211,6 +211,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const positionerElement = renderElement(); const options = findSelectOptions(positionerElement); + const mountedItemsElement = keepMounted ? null : ; const nativeSelectElement = ( - {options.map((item) => ( - // eslint-disable-next-line jsx-a11y/control-has-associated-label - ); From d736ac5acc43b23f5885512a74cebecca0b4919c Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 2 Sep 2024 20:53:17 +1000 Subject: [PATCH 25/94] Fix selectionRef type --- .../base/components/select/SelectIntroduction/system/index.js | 4 ++-- .../components/select/SelectIntroduction/system/index.tsx | 4 ++-- packages/mui-base/src/Select/Option/SelectOption.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index b15819b2a..a93eabf1c 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -65,9 +65,9 @@ const entries = Object.entries(data); export default function UnstyledSelectIntroduction() { return ( - + - + diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index b15819b2a..a93eabf1c 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -65,9 +65,9 @@ const entries = Object.entries(data); export default function UnstyledSelectIntroduction() { return ( - + - + diff --git a/packages/mui-base/src/Select/Option/SelectOption.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx index 42cc0d871..fab9884d7 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -161,8 +161,8 @@ interface InnerSelectOptionProps extends Omit { typingRef: React.MutableRefObject; handleSelect: () => void; selectionRef: React.MutableRefObject<{ - mouseUp: boolean; - select: boolean; + allowMouseUp: boolean; + allowSelect: boolean; }>; open: boolean; } From 8d34e7ec36243ff9557227355f8c6739d978b7b0 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 4 Sep 2024 16:54:35 +1000 Subject: [PATCH 26/94] Update APIs --- .../select/SelectIntroduction/system/index.js | 59 ++++++++++--------- .../SelectIntroduction/system/index.tsx | 59 ++++++++++--------- docs/data/base/components/select/select.md | 25 ++++++-- docs/data/base/pagesApi.js | 8 ++- docs/pages/base-ui/api/select-root.json | 5 +- .../base-ui/api/select-scroll-arrow.json | 19 ------ .../base-ui/api/select-scroll-down-arrow.json | 16 +++++ .../base-ui/api/select-scroll-up-arrow.json | 16 +++++ .../base-ui/react-select/[docsTab]/index.js | 24 +++++--- .../api-docs/select-root/select-root.json | 4 +- .../select-scroll-arrow.json | 10 ---- .../select-scroll-down-arrow.json | 9 +++ .../select-scroll-up-arrow.json | 9 +++ .../src/Select/Backdrop/SelectBackdrop.tsx | 8 +-- .../src/Select/Popup/useSelectPopup.ts | 6 +- .../Select/Positioner/SelectPositioner.tsx | 4 +- .../Select/Positioner/useSelectPositioner.tsx | 5 +- .../mui-base/src/Select/Root/SelectRoot.tsx | 22 +++---- .../src/Select/Root/SelectRootContext.ts | 2 +- .../src/Select/Root/useSelectRoot.tsx | 11 ++-- .../Select/ScrollArrow/SelectScrollArrow.tsx | 25 ++++++-- .../SelectScrollDownArrow.test.tsx} | 4 +- .../ScrollDownArrow/SelectScrollDownArrow.tsx | 41 +++++++++++++ .../SelectScrollUpArrow.test.tsx | 18 ++++++ .../ScrollUpArrow/SelectScrollUpArrow.tsx | 41 +++++++++++++ packages/mui-base/src/Select/index.barrel.ts | 3 +- packages/mui-base/src/Select/index.ts | 3 +- .../src/utils/useAnchorPositioning.ts | 31 ++++------ 28 files changed, 331 insertions(+), 156 deletions(-) delete mode 100644 docs/pages/base-ui/api/select-scroll-arrow.json create mode 100644 docs/pages/base-ui/api/select-scroll-down-arrow.json create mode 100644 docs/pages/base-ui/api/select-scroll-up-arrow.json delete mode 100644 docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json create mode 100644 docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json create mode 100644 docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json rename packages/mui-base/src/Select/{ScrollArrow/SelectScrollArrow.test.tsx => ScrollDownArrow/SelectScrollDownArrow.test.tsx} (78%) create mode 100644 packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx create mode 100644 packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx create mode 100644 packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index a93eabf1c..432b57166 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -2,7 +2,7 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; -import { styled } from '@mui/system'; +import { css, styled } from '@mui/system'; import Check from '@mui/icons-material/Check'; import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; @@ -71,19 +71,16 @@ export default function UnstyledSelectIntroduction() { - - + +
        -
        + + } /> Select food... - } - keepMounted={false} - /> {entries.map(([group, items]) => ( @@ -96,22 +93,19 @@ export default function UnstyledSelectIntroduction() { value={item.value} disabled={item.value === 'banana'} > + } /> {item.label} - } - keepMounted={false} - />
        ))}
        ))}
        - +
        -
        +
        ); @@ -154,10 +148,11 @@ const SelectPopup = styled(Select.Popup)` 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; + min-width: calc(var(--anchor-width) + 16px); `; const SelectOption = styled(Select.Option)` - padding: 6px 12px; + padding: 6px 16px 6px 4px; outline: 0; cursor: default; border-radius: 4px; @@ -165,7 +160,6 @@ const SelectOption = styled(Select.Option)` user-select: none; display: flex; align-items: center; - justify-content: space-between; line-height: 1.5; scroll-margin: 15px; @@ -181,29 +175,27 @@ const SelectOption = styled(Select.Option)` `; const SelectOptionIndicator = styled(Select.OptionIndicator)` - margin-left: 8px; + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } `; const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` font-weight: bold; - padding: 4px 12px; + padding: 4px 24px; cursor: default; user-select: none; `; -const SelectScrollArrow = styled(Select.ScrollArrow)` +const scrollArrowStyles = css` width: 100%; height: 25px; - &[data-direction='up'] { - transform: rotate(180deg); - top: -10px; - } - - &[data-direction='down'] { - bottom: -10px; - } - > div { position: absolute; background: white; @@ -217,6 +209,17 @@ const SelectScrollArrow = styled(Select.ScrollArrow)` } `; +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: -10px; + ${scrollArrowStyles} +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: -10px; + ${scrollArrowStyles} +`; + const SelectSeparator = styled(Select.Separator)` height: 1px; background-color: ${gray[300]}; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index a93eabf1c..432b57166 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; -import { styled } from '@mui/system'; +import { css, styled } from '@mui/system'; import Check from '@mui/icons-material/Check'; import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; @@ -71,19 +71,16 @@ export default function UnstyledSelectIntroduction() { - - + +
        -
        + + } /> Select food... - } - keepMounted={false} - /> {entries.map(([group, items]) => ( @@ -96,22 +93,19 @@ export default function UnstyledSelectIntroduction() { value={item.value} disabled={item.value === 'banana'} > + } /> {item.label} - } - keepMounted={false} - /> ))} ))} - +
        -
        +
        ); @@ -154,10 +148,11 @@ const SelectPopup = styled(Select.Popup)` 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; + min-width: calc(var(--anchor-width) + 16px); `; const SelectOption = styled(Select.Option)` - padding: 6px 12px; + padding: 6px 16px 6px 4px; outline: 0; cursor: default; border-radius: 4px; @@ -165,7 +160,6 @@ const SelectOption = styled(Select.Option)` user-select: none; display: flex; align-items: center; - justify-content: space-between; line-height: 1.5; scroll-margin: 15px; @@ -181,29 +175,27 @@ const SelectOption = styled(Select.Option)` `; const SelectOptionIndicator = styled(Select.OptionIndicator)` - margin-left: 8px; + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } `; const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` font-weight: bold; - padding: 4px 12px; + padding: 4px 24px; cursor: default; user-select: none; `; -const SelectScrollArrow = styled(Select.ScrollArrow)` +const scrollArrowStyles = css` width: 100%; height: 25px; - &[data-direction='up'] { - transform: rotate(180deg); - top: -10px; - } - - &[data-direction='down'] { - bottom: -10px; - } - > div { position: absolute; background: white; @@ -217,6 +209,17 @@ const SelectScrollArrow = styled(Select.ScrollArrow)` } `; +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: -10px; + ${scrollArrowStyles} +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: -10px; + ${scrollArrowStyles} +`; + const SelectSeparator = styled(Select.Separator)` height: 1px; background-color: ${gray[300]}; diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md index 097ea8980..549d6bee5 100644 --- a/docs/data/base/components/select/select.md +++ b/docs/data/base/components/select/select.md @@ -1,7 +1,7 @@ --- productId: base-ui title: React Select components and hook -components: SelectRoot, SelectTrigger, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectValue, SelectScrollArrow, SelectSeparator +components: SelectRoot, SelectTrigger, SelectValue, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- @@ -56,7 +56,9 @@ Selects are implemented using a collection of related components: - `` renders an option indicator inside an option to indicate it's selected (e.g. a check icon). - `` renders an option group, wrapping `` components. - `` renders a label for an option group. -- `` renders a scrolling arrow for the `alignToItem` (default) anchoring mode. +- `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. +- `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. +- `` renders a separator between option groups. ```jsx @@ -67,7 +69,7 @@ Selects are implemented using a collection of related components: - + @@ -79,7 +81,22 @@ Selects are implemented using a collection of related components: - + ``` + +## Align method + +Two different methods to align the popup are available: + +- `alignMethod="item"` (default) +- `alignMethod="trigger"` + +```jsx + +``` + +The `item` method aligns the popup such that the selected item inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. + +The `trigger` method aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 3edbce811..9b75c40f7 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -257,8 +257,12 @@ module.exports = [ title: 'SelectRoot', }, { - pathname: '/base-ui/react-select/components-api/#select-scroll-arrow', - title: 'SelectScrollArrow', + pathname: '/base-ui/react-select/components-api/#select-scroll-down-arrow', + title: 'SelectScrollDownArrow', + }, + { + pathname: '/base-ui/react-select/components-api/#select-scroll-up-arrow', + title: 'SelectScrollUpArrow', }, { pathname: '/base-ui/react-select/components-api/#select-separator', diff --git a/docs/pages/base-ui/api/select-root.json b/docs/pages/base-ui/api/select-root.json index 9ec7b965b..09074d827 100644 --- a/docs/pages/base-ui/api/select-root.json +++ b/docs/pages/base-ui/api/select-root.json @@ -1,6 +1,9 @@ { "props": { - "alignToItem": { "type": { "name": "bool" }, "default": "true" }, + "alignMethod": { + "type": { "name": "enum", "description": "'item'
        | 'trigger'" }, + "default": "'item'" + }, "animated": { "type": { "name": "bool" }, "default": "true" }, "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, "defaultValue": { "type": { "name": "string" } }, diff --git a/docs/pages/base-ui/api/select-scroll-arrow.json b/docs/pages/base-ui/api/select-scroll-arrow.json deleted file mode 100644 index 1105fd689..000000000 --- a/docs/pages/base-ui/api/select-scroll-arrow.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "props": { - "className": { "type": { "name": "union", "description": "func
        | string" } }, - "render": { "type": { "name": "union", "description": "element
        | func" } } - }, - "name": "SelectScrollArrow", - "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectScrollArrow = Select.ScrollArrow;" - ], - "classes": [], - "spread": true, - "themeDefaultProps": true, - "muiName": "SelectScrollArrow", - "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx", - "inheritance": null, - "demos": "", - "cssComponent": false -} diff --git a/docs/pages/base-ui/api/select-scroll-down-arrow.json b/docs/pages/base-ui/api/select-scroll-down-arrow.json new file mode 100644 index 000000000..267439cf9 --- /dev/null +++ b/docs/pages/base-ui/api/select-scroll-down-arrow.json @@ -0,0 +1,16 @@ +{ + "props": { "keepMounted": { "type": { "name": "bool" }, "default": "false" } }, + "name": "SelectScrollDownArrow", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectScrollDownArrow = Select.ScrollDownArrow;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectScrollDownArrow", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-scroll-up-arrow.json b/docs/pages/base-ui/api/select-scroll-up-arrow.json new file mode 100644 index 000000000..ad5f0d8e5 --- /dev/null +++ b/docs/pages/base-ui/api/select-scroll-up-arrow.json @@ -0,0 +1,16 @@ +{ + "props": { "keepMounted": { "type": { "name": "bool" }, "default": "false" } }, + "name": "SelectScrollUpArrow", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectScrollUpArrow = Select.ScrollUpArrow;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectScrollUpArrow", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js index fcefdc028..68e14953c 100644 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -11,7 +11,8 @@ import SelectOptionIndicatorApiJsonPageContent from '../../api/select-option-ind import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; import SelectPositionerApiJsonPageContent from '../../api/select-positioner.json'; import SelectRootApiJsonPageContent from '../../api/select-root.json'; -import SelectScrollArrowApiJsonPageContent from '../../api/select-scroll-arrow.json'; +import SelectScrollDownArrowApiJsonPageContent from '../../api/select-scroll-down-arrow.json'; +import SelectScrollUpArrowApiJsonPageContent from '../../api/select-scroll-up-arrow.json'; import SelectSeparatorApiJsonPageContent from '../../api/select-separator.json'; import SelectTriggerApiJsonPageContent from '../../api/select-trigger.json'; import SelectValueApiJsonPageContent from '../../api/select-value.json'; @@ -91,12 +92,19 @@ export const getStaticProps = () => { ); const SelectRootApiDescriptions = mapApiPageTranslations(SelectRootApiReq); - const SelectScrollArrowApiReq = require.context( - 'docs-base/translations/api-docs/select-scroll-arrow', + const SelectScrollDownArrowApiReq = require.context( + 'docs-base/translations/api-docs/select-scroll-down-arrow', false, - /\.\/select-scroll-arrow.*.json$/, + /\.\/select-scroll-down-arrow.*.json$/, ); - const SelectScrollArrowApiDescriptions = mapApiPageTranslations(SelectScrollArrowApiReq); + const SelectScrollDownArrowApiDescriptions = mapApiPageTranslations(SelectScrollDownArrowApiReq); + + const SelectScrollUpArrowApiReq = require.context( + 'docs-base/translations/api-docs/select-scroll-up-arrow', + false, + /\.\/select-scroll-up-arrow.*.json$/, + ); + const SelectScrollUpArrowApiDescriptions = mapApiPageTranslations(SelectScrollUpArrowApiReq); const SelectSeparatorApiReq = require.context( 'docs-base/translations/api-docs/select-separator', @@ -130,7 +138,8 @@ export const getStaticProps = () => { SelectPopup: SelectPopupApiDescriptions, SelectPositioner: SelectPositionerApiDescriptions, SelectRoot: SelectRootApiDescriptions, - SelectScrollArrow: SelectScrollArrowApiDescriptions, + SelectScrollDownArrow: SelectScrollDownArrowApiDescriptions, + SelectScrollUpArrow: SelectScrollUpArrowApiDescriptions, SelectSeparator: SelectSeparatorApiDescriptions, SelectTrigger: SelectTriggerApiDescriptions, SelectValue: SelectValueApiDescriptions, @@ -144,7 +153,8 @@ export const getStaticProps = () => { SelectPopup: SelectPopupApiJsonPageContent, SelectPositioner: SelectPositionerApiJsonPageContent, SelectRoot: SelectRootApiJsonPageContent, - SelectScrollArrow: SelectScrollArrowApiJsonPageContent, + SelectScrollDownArrow: SelectScrollDownArrowApiJsonPageContent, + SelectScrollUpArrow: SelectScrollUpArrowApiJsonPageContent, SelectSeparator: SelectSeparatorApiJsonPageContent, SelectTrigger: SelectTriggerApiJsonPageContent, SelectValue: SelectValueApiJsonPageContent, diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json index 103d8a6d3..df1f10fce 100644 --- a/docs/translations/api-docs/select-root/select-root.json +++ b/docs/translations/api-docs/select-root/select-root.json @@ -1,7 +1,9 @@ { "componentDescription": "", "propDescriptions": { - "alignToItem": { "description": "Determines if the Select should align to the item." }, + "alignMethod": { + "description": "Determines if the select should align to the selected item inside the popup or the trigger element." + }, "animated": { "description": "If true, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." }, diff --git a/docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json b/docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json deleted file mode 100644 index 4bc12cf1e..000000000 --- a/docs/translations/api-docs/select-scroll-arrow/select-scroll-arrow.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json b/docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json new file mode 100644 index 000000000..47e423952 --- /dev/null +++ b/docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "keepMounted": { + "description": "Whether the component should be kept mounted when it is not rendered." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json b/docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json new file mode 100644 index 000000000..47e423952 --- /dev/null +++ b/docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "keepMounted": { + "description": "Whether the component should be kept mounted when it is not rendered." + } + }, + "classDescriptions": {} +} diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx index eea9f4184..b93957c27 100644 --- a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx @@ -6,7 +6,6 @@ import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { HTMLElementType } from '../../utils/proptypes'; import { useForkRef } from '../../utils/useForkRef'; -import { useScrollLock } from '../../utils/useScrollLock'; import { useSelectRootContext } from '../Root/SelectRootContext'; import { useSelectBackdrop } from './useSelectBackdrop'; import { commonStyleHooks } from '../utils/commonStyleHooks'; @@ -20,8 +19,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop( ) { const { className, render, keepMounted = false, container, ...otherProps } = props; - const { open, mounted, innerFallback, alignToItem, selectedIndex, backdropRef } = - useSelectRootContext(); + const { open, mounted, backdropRef } = useSelectRootContext(); const { getBackdropProps } = useSelectBackdrop(); @@ -29,10 +27,6 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop( const ownerState: SelectBackdrop.OwnerState = React.useMemo(() => ({ open }), [open]); - const standardMode = !(selectedIndex !== null && alignToItem && !innerFallback); - - useScrollLock(!standardMode && mounted); - const { renderElement } = useComponentRenderer({ propGetter: getBackdropProps, render: render ?? 'div', diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 7d53916ce..a4a9bcec9 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -12,7 +12,7 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; export function useSelectPopup(): useSelectPopup.ReturnValue { const { getPopupProps: getRootPopupProps, - alignToItem, + alignMethod, selectedIndex, innerFallback, touchModality, @@ -27,7 +27,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { // must be relative to the element. position: 'relative', overflowY: 'auto', - ...(alignToItem && + ...(alignMethod && hasSelectedIndex && !innerFallback && !touchModality && { @@ -36,7 +36,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { }, }); }, - [getRootPopupProps, alignToItem, hasSelectedIndex, innerFallback, touchModality], + [getRootPopupProps, alignMethod, hasSelectedIndex, innerFallback, touchModality], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 08656d271..de6ffe94f 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -68,7 +68,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( popupRef, overflowRef, innerOffset, - alignToItem, + alignMethod, innerFallback, setInnerFallback, selectedIndex, @@ -129,7 +129,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( allowAxisFlip: false, innerFallback, inner: - alignToItem && selectedIndexOnMount !== null + alignMethod && selectedIndexOnMount !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 6e03aa118..59302a38c 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -12,6 +12,7 @@ import type { GenericHTMLProps } from '../../utils/types'; import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useScrollLock } from '../../utils/useScrollLock'; /** * @@ -24,7 +25,9 @@ export function useSelectPositioner( ): useSelectPositioner.ReturnValue { const { open = false, keepMounted } = params; - const { touchModality } = useSelectRootContext(); + const { touchModality, alignMethod, innerFallback, mounted } = useSelectRootContext(); + + useScrollLock(alignMethod && !innerFallback && mounted); const { positionerStyles, diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index 3cf153582..db35af1dd 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -19,7 +19,7 @@ function SelectRoot(props: SelectRoot.Props) { loop = true, onOpenChange, open, - alignToItem = true, + alignMethod = 'item', } = props; const selectRoot = useSelectRoot({ @@ -29,7 +29,7 @@ function SelectRoot(props: SelectRoot.Props) { loop, defaultOpen, open, - alignToItem, + alignMethod, value, defaultValue, }); @@ -38,13 +38,13 @@ function SelectRoot(props: SelectRoot.Props) { () => ({ ...selectRoot, disabled, - alignToItem, + alignMethod, id, name, required, readOnly, }), - [selectRoot, disabled, alignToItem, id, name, required, readOnly], + [selectRoot, disabled, alignMethod, id, name, required, readOnly], ); return {children}; @@ -113,10 +113,11 @@ namespace SelectRoot { */ open?: boolean; /** - * Determines if the Select should align to the item. - * @default true + * Determines if the select should align to the selected item inside the popup or the trigger + * element. + * @default 'item' */ - alignToItem?: boolean; + alignMethod?: 'item' | 'trigger'; } } @@ -126,10 +127,11 @@ SelectRoot.propTypes /* remove-proptypes */ = { // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** - * Determines if the Select should align to the item. - * @default true + * Determines if the select should align to the selected item inside the popup or the trigger + * element. + * @default 'item' */ - alignToItem: PropTypes.bool, + alignMethod: PropTypes.oneOf(['item', 'trigger']), /** * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. diff --git a/packages/mui-base/src/Select/Root/SelectRootContext.ts b/packages/mui-base/src/Select/Root/SelectRootContext.ts index 82bf01dcd..dedbcc109 100644 --- a/packages/mui-base/src/Select/Root/SelectRootContext.ts +++ b/packages/mui-base/src/Select/Root/SelectRootContext.ts @@ -7,7 +7,7 @@ export interface SelectRootContext extends useSelectRoot.ReturnValue, useFieldControlValidation.ReturnValue { typingRef: React.MutableRefObject; - alignToItem: boolean; + alignMethod: 'item' | 'trigger'; id: string | undefined; name: string | undefined; disabled: boolean; diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 45f1de988..27ca15cb9 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -40,7 +40,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R value: valueProp, onValueChange, defaultValue = '', - alignToItem, + alignMethod, } = params; const { setDirty, validityData, validateOnChange } = useFieldRootContext(); @@ -184,7 +184,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R }); const innerOffsetInteractionProps = useInnerOffset(floatingRootContext, { - enabled: alignToItem && !innerFallback, + enabled: alignMethod && !innerFallback, onChange: setInnerOffset, scrollRef: popupRef, overflowRef, @@ -313,10 +313,11 @@ export namespace useSelectRoot { */ defaultValue?: string; /** - * If `true`, the Select will align to the selected item. - * @default true + * Determines if the select should align to the selected item inside the popup or the trigger + * element. + * @default 'item' */ - alignToItem?: boolean; + alignMethod?: 'item' | 'trigger'; } export interface ReturnValue extends useFieldControlValidation.ReturnValue { diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index dd377b114..303115725 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -8,19 +8,22 @@ import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; import { useEventCallback } from '../../utils/useEventCallback'; +/** + * @ignore - internal component. + */ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( props: SelectScrollArrow.Props, forwardedRef: React.ForwardedRef, ) { - const { render, className, direction, ...otherProps } = props; + const { render, className, direction, keepMounted = false, ...otherProps } = props; - const { innerOffset, setInnerOffset, innerFallback, alignToItem, popupRef, touchModality } = + const { innerOffset, setInnerOffset, innerFallback, alignMethod, popupRef, touchModality } = useSelectRootContext(); const { isPositioned } = useSelectPositionerContext(); const [rendered, setRendered] = React.useState(false); - const inert = !(!touchModality && alignToItem && !innerFallback); + const inert = !(!touchModality && alignMethod && !innerFallback); if (rendered && inert) { setRendered(false); @@ -31,8 +34,9 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( const ownerState: SelectScrollArrow.OwnerState = React.useMemo( () => ({ direction, + rendered, }), - [direction], + [direction, rendered], ); const getScrollArrowProps = React.useCallback( @@ -146,7 +150,8 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( extraProps: otherProps, }); - if (!rendered) { + const shouldRender = rendered || keepMounted; + if (!shouldRender) { return null; } @@ -159,6 +164,11 @@ namespace SelectScrollArrow { } export interface Props extends BaseUIComponentProps<'div', OwnerState> { direction: 'up' | 'down'; + /** + * Whether the component should be kept mounted when it is not rendered. + * @default false + */ + keepMounted?: boolean; } } @@ -179,6 +189,11 @@ SelectScrollArrow.propTypes /* remove-proptypes */ = { * @ignore */ direction: PropTypes.oneOf(['down', 'up']).isRequired, + /** + * Whether the component should be kept mounted when it is not rendered. + * @default false + */ + keepMounted: PropTypes.bool, /** * A function to customize rendering of the component. */ diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.test.tsx b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx similarity index 78% rename from packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.test.tsx rename to packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx index ee3869d3a..9c985753c 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.test.tsx +++ b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx @@ -2,10 +2,10 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; -describe('', () => { +describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( diff --git a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx new file mode 100644 index 000000000..c55079d89 --- /dev/null +++ b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx @@ -0,0 +1,41 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { SelectScrollArrow } from '../ScrollArrow/SelectScrollArrow'; +import type { BaseUIComponentProps } from '../../utils/types'; + +const SelectScrollDownArrow = React.forwardRef(function SelectScrollDownArrow( + props: SelectScrollDownArrow.Props, + forwardedRef: React.ForwardedRef, +) { + return ; +}); + +namespace SelectScrollDownArrow { + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + /** + * Whether the component should be kept mounted when it is not rendered. + * @default false + */ + keepMounted?: boolean; + } +} + +SelectScrollDownArrow.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Whether the component should be kept mounted when it is not rendered. + * @default false + */ + keepMounted: PropTypes.bool, +} as any; + +export { SelectScrollDownArrow }; diff --git a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx new file mode 100644 index 000000000..2134c2c4c --- /dev/null +++ b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx new file mode 100644 index 000000000..80d231771 --- /dev/null +++ b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx @@ -0,0 +1,41 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { SelectScrollArrow } from '../ScrollArrow/SelectScrollArrow'; +import type { BaseUIComponentProps } from '../../utils/types'; + +const SelectScrollUpArrow = React.forwardRef(function SelectScrollUpArrow( + props: SelectScrollUpArrow.Props, + forwardedRef: React.ForwardedRef, +) { + return ; +}); + +namespace SelectScrollUpArrow { + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + /** + * Whether the component should be kept mounted when it is not rendered. + * @default false + */ + keepMounted?: boolean; + } +} + +SelectScrollUpArrow.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Whether the component should be kept mounted when it is not rendered. + * @default false + */ + keepMounted: PropTypes.bool, +} as any; + +export { SelectScrollUpArrow }; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index 039a99606..b9ae364e7 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -8,5 +8,6 @@ export { SelectOptionIndicator } from './OptionIndicator/SelectOptionIndicator'; export { SelectOptionGroup } from './OptionGroup/SelectOptionGroup'; export { SelectOptionGroupLabel } from './OptionGroupLabel/SelectOptionGroupLabel'; export { SelectValue } from './Value/SelectValue'; -export { SelectScrollArrow } from './ScrollArrow/SelectScrollArrow'; +export { SelectScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; +export { SelectScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; export { SelectSeparator } from './Separator/SelectSeparator'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 623adea40..1c87eb63c 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -8,5 +8,6 @@ export { SelectOptionIndicator as OptionIndicator } from './OptionIndicator/Sele export { SelectOptionGroup as OptionGroup } from './OptionGroup/SelectOptionGroup'; export { SelectOptionGroupLabel as OptionGroupLabel } from './OptionGroupLabel/SelectOptionGroupLabel'; export { SelectValue as Value } from './Value/SelectValue'; -export { SelectScrollArrow as ScrollArrow } from './ScrollArrow/SelectScrollArrow'; +export { SelectScrollUpArrow as ScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; +export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; export { SelectSeparator as Separator } from './Separator/SelectSeparator'; diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index 4725d07bd..1f121574c 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -159,25 +159,20 @@ export function useAnchorPositioning( innerOptions.touchModality ? shift({ crossAxis: true, ...commonCollisionProps }) : (false as const), - size({ - ...commonCollisionProps, - apply({ - elements: { floating }, - rects: { reference }, - availableWidth, - availableHeight, - }) { - Object.entries({ - '--available-width': `${availableWidth}px`, - '--available-height': `${availableHeight}px`, - '--anchor-width': `${reference.width}px`, - '--anchor-height': `${reference.height}px`, - }).forEach(([key, value]) => { - floating.style.setProperty(key, value); - }); - }, - }), ]), + size({ + ...commonCollisionProps, + apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) { + Object.entries({ + '--available-width': `${availableWidth}px`, + '--available-height': `${availableHeight}px`, + '--anchor-width': `${reference.width}px`, + '--anchor-height': `${reference.height}px`, + }).forEach(([key, value]) => { + floating.style.setProperty(key, value); + }); + }, + }), arrow( () => ({ // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set, From ee457767467f2d2f0cca5d85c2a29aaeca21c0d3 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 12:59:38 +1000 Subject: [PATCH 27/94] Add Icon, Arrow components --- .../select/SelectIntroduction/system/index.js | 25 ++++- .../SelectIntroduction/system/index.tsx | 25 ++++- docs/data/base/components/select/select.md | 25 ++++- docs/data/base/pagesApi.js | 8 ++ docs/pages/base-ui/api/select-arrow.json | 20 ++++ docs/pages/base-ui/api/select-icon.json | 17 +++ .../base-ui/react-select/[docsTab]/index.js | 20 ++++ .../api-docs/select-arrow/select-arrow.json | 13 +++ .../api-docs/select-icon/select-icon.json | 10 ++ .../src/Select/Arrow/SelectArrow.test.tsx | 18 ++++ .../mui-base/src/Select/Arrow/SelectArrow.tsx | 101 ++++++++++++++++++ .../src/Select/Icon/SelectIcon.test.tsx | 18 ++++ .../mui-base/src/Select/Icon/SelectIcon.tsx | 59 ++++++++++ .../OptionIndicator/SelectOptionIndicator.tsx | 10 ++ .../mui-base/src/Select/Popup/SelectPopup.tsx | 2 +- .../Select/Positioner/SelectPositioner.tsx | 6 +- .../Positioner/SelectPositionerContext.ts | 2 +- .../Select/Positioner/useSelectPositioner.tsx | 6 +- .../Select/ScrollArrow/SelectScrollArrow.tsx | 34 +++--- packages/mui-base/src/Select/index.barrel.ts | 2 + packages/mui-base/src/Select/index.ts | 2 + 21 files changed, 392 insertions(+), 31 deletions(-) create mode 100644 docs/pages/base-ui/api/select-arrow.json create mode 100644 docs/pages/base-ui/api/select-icon.json create mode 100644 docs/translations/api-docs/select-arrow/select-arrow.json create mode 100644 docs/translations/api-docs/select-icon/select-icon.json create mode 100644 packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx create mode 100644 packages/mui-base/src/Select/Arrow/SelectArrow.tsx create mode 100644 packages/mui-base/src/Select/Icon/SelectIcon.test.tsx create mode 100644 packages/mui-base/src/Select/Icon/SelectIcon.tsx diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 432b57166..8f1769352 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -135,8 +135,11 @@ const SelectTrigger = styled(Select.Trigger)` } `; -const SelectDropdownArrow = styled(ArrowDropDown)` - margin-right: -6px; +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; `; const SelectPopup = styled(Select.Popup)` @@ -194,7 +197,11 @@ const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` const scrollArrowStyles = css` width: 100%; - height: 25px; + height: 15px; + + &[data-side='none'] { + height: 25px; + } > div { position: absolute; @@ -211,13 +218,21 @@ const scrollArrowStyles = css` const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` transform: rotate(180deg); - top: -10px; + top: 0; ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: -10px; + bottom: 0; ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } `; const SelectSeparator = styled(Select.Separator)` diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 432b57166..8f1769352 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -135,8 +135,11 @@ const SelectTrigger = styled(Select.Trigger)` } `; -const SelectDropdownArrow = styled(ArrowDropDown)` - margin-right: -6px; +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; `; const SelectPopup = styled(Select.Popup)` @@ -194,7 +197,11 @@ const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` const scrollArrowStyles = css` width: 100%; - height: 25px; + height: 15px; + + &[data-side='none'] { + height: 25px; + } > div { position: absolute; @@ -211,13 +218,21 @@ const scrollArrowStyles = css` const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` transform: rotate(180deg); - top: -10px; + top: 0; ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: -10px; + bottom: 0; ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } `; const SelectSeparator = styled(Select.Separator)` diff --git a/docs/data/base/components/select/select.md b/docs/data/base/components/select/select.md index 549d6bee5..4c5111916 100644 --- a/docs/data/base/components/select/select.md +++ b/docs/data/base/components/select/select.md @@ -1,7 +1,7 @@ --- productId: base-ui title: React Select components and hook -components: SelectRoot, SelectTrigger, SelectValue, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator +components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- @@ -49,6 +49,7 @@ Selects are implemented using a collection of related components: - `` is a top-level component that wraps the other components. - `` renders the trigger element that opens the select popup on click. - `` renders the value of the select. +- `` renders a caret icon. - `` renders a backdrop element behind the popup. - `` renders the select popup's positioning element. - `` renders the select popup itself. @@ -59,11 +60,13 @@ Selects are implemented using a collection of related components: - `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. - `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. - `` renders a separator between option groups. +- `` renders the select popup's arrow when using `alignMethod="trigger"`. ```jsx + @@ -81,6 +84,8 @@ Selects are implemented using a collection of related components: + + @@ -100,3 +105,21 @@ Two different methods to align the popup are available: The `item` method aligns the popup such that the selected item inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. The `trigger` method aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. + +## Value + +The `Select.Value` subcomponent renders the selected value. This is the text content or `label` of `Select.Option` by default. + +The `placeholder` prop can be used when the value is empty. During SSR, if a default value is specified as the selected option, the value isn't available until hydration: + +```jsx + + + +``` + +A function can be specified as a child to customize the rendering of the value: + +```jsx +{(value) => {value}} +``` diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 9b75c40f7..34260360c 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -224,10 +224,18 @@ module.exports = [ pathname: '/base-ui/react-progress/components-api/#progress-track', title: 'ProgressTrack', }, + { + pathname: '/base-ui/react-select/components-api/#select-arrow', + title: 'SelectArrow', + }, { pathname: '/base-ui/react-select/components-api/#select-backdrop', title: 'SelectBackdrop', }, + { + pathname: '/base-ui/react-select/components-api/#select-icon', + title: 'SelectIcon', + }, { pathname: '/base-ui/react-select/components-api/#select-option', title: 'SelectOption', diff --git a/docs/pages/base-ui/api/select-arrow.json b/docs/pages/base-ui/api/select-arrow.json new file mode 100644 index 000000000..1cf176708 --- /dev/null +++ b/docs/pages/base-ui/api/select-arrow.json @@ -0,0 +1,20 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
        | string" } }, + "hideWhenUncentered": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
        | func" } } + }, + "name": "SelectArrow", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectArrow = Select.Arrow;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectArrow", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/Arrow/SelectArrow.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-icon.json b/docs/pages/base-ui/api/select-icon.json new file mode 100644 index 000000000..f3ac336a3 --- /dev/null +++ b/docs/pages/base-ui/api/select-icon.json @@ -0,0 +1,17 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
        | string" } }, + "render": { "type": { "name": "union", "description": "element
        | func" } } + }, + "name": "SelectIcon", + "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectIcon = Select.Icon;"], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectIcon", + "forwardsRefTo": "HTMLSpanElement", + "filename": "/packages/mui-base/src/Select/Icon/SelectIcon.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js index 68e14953c..384697de2 100644 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ b/docs/pages/base-ui/react-select/[docsTab]/index.js @@ -3,7 +3,9 @@ import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; import AppFrame from 'docs/src/modules/components/AppFrame'; import * as pageProps from 'docs-base/data/base/components/select/select.md?@mui/markdown'; import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import SelectArrowApiJsonPageContent from '../../api/select-arrow.json'; import SelectBackdropApiJsonPageContent from '../../api/select-backdrop.json'; +import SelectIconApiJsonPageContent from '../../api/select-icon.json'; import SelectOptionApiJsonPageContent from '../../api/select-option.json'; import SelectOptionGroupApiJsonPageContent from '../../api/select-option-group.json'; import SelectOptionGroupLabelApiJsonPageContent from '../../api/select-option-group-label.json'; @@ -34,6 +36,13 @@ export const getStaticPaths = () => { }; export const getStaticProps = () => { + const SelectArrowApiReq = require.context( + 'docs-base/translations/api-docs/select-arrow', + false, + /\.\/select-arrow.*.json$/, + ); + const SelectArrowApiDescriptions = mapApiPageTranslations(SelectArrowApiReq); + const SelectBackdropApiReq = require.context( 'docs-base/translations/api-docs/select-backdrop', false, @@ -41,6 +50,13 @@ export const getStaticProps = () => { ); const SelectBackdropApiDescriptions = mapApiPageTranslations(SelectBackdropApiReq); + const SelectIconApiReq = require.context( + 'docs-base/translations/api-docs/select-icon', + false, + /\.\/select-icon.*.json$/, + ); + const SelectIconApiDescriptions = mapApiPageTranslations(SelectIconApiReq); + const SelectOptionApiReq = require.context( 'docs-base/translations/api-docs/select-option', false, @@ -130,7 +146,9 @@ export const getStaticProps = () => { return { props: { componentsApiDescriptions: { + SelectArrow: SelectArrowApiDescriptions, SelectBackdrop: SelectBackdropApiDescriptions, + SelectIcon: SelectIconApiDescriptions, SelectOption: SelectOptionApiDescriptions, SelectOptionGroup: SelectOptionGroupApiDescriptions, SelectOptionGroupLabel: SelectOptionGroupLabelApiDescriptions, @@ -145,7 +163,9 @@ export const getStaticProps = () => { SelectValue: SelectValueApiDescriptions, }, componentsApiPageContents: { + SelectArrow: SelectArrowApiJsonPageContent, SelectBackdrop: SelectBackdropApiJsonPageContent, + SelectIcon: SelectIconApiJsonPageContent, SelectOption: SelectOptionApiJsonPageContent, SelectOptionGroup: SelectOptionGroupApiJsonPageContent, SelectOptionGroupLabel: SelectOptionGroupLabelApiJsonPageContent, diff --git a/docs/translations/api-docs/select-arrow/select-arrow.json b/docs/translations/api-docs/select-arrow/select-arrow.json new file mode 100644 index 000000000..3dfaaeafb --- /dev/null +++ b/docs/translations/api-docs/select-arrow/select-arrow.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "hideWhenUncentered": { + "description": "If true, the arrow is hidden when it can't point to the center of the anchor element." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/select-icon/select-icon.json b/docs/translations/api-docs/select-icon/select-icon.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/translations/api-docs/select-icon/select-icon.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx new file mode 100644 index 000000000..2c2b6077a --- /dev/null +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx new file mode 100644 index 000000000..eccf5f9da --- /dev/null +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx @@ -0,0 +1,101 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useForkRef } from '../../utils/useForkRef'; +import { mergeReactProps } from '../../utils/mergeReactProps'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { commonStyleHooks } from '../utils/commonStyleHooks'; + +const SelectArrow = React.forwardRef(function SelectArrow( + props: SelectArrow.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, hideWhenUncentered = false, ...otherProps } = props; + + const { open, alignMethod } = useSelectRootContext(); + const { arrowRef, side, alignment, arrowUncentered, arrowStyles } = useSelectPositionerContext(); + + const getArrowProps = React.useCallback( + (externalProps = {}) => + mergeReactProps<'div'>(externalProps, { + style: { + ...arrowStyles, + ...(hideWhenUncentered && arrowUncentered ? { visibility: 'hidden' } : {}), + }, + }), + [arrowStyles, hideWhenUncentered, arrowUncentered], + ); + + const ownerState: SelectArrow.OwnerState = React.useMemo( + () => ({ + open, + side, + alignment, + arrowUncentered, + }), + [open, side, alignment, arrowUncentered], + ); + + const mergedRef = useForkRef(arrowRef, forwardedRef); + + const { renderElement } = useComponentRenderer({ + propGetter: getArrowProps, + render: render ?? 'div', + className, + ownerState, + ref: mergedRef, + extraProps: otherProps, + customStyleHookMapping: commonStyleHooks, + }); + + if (alignMethod !== 'trigger') { + return null; + } + + return renderElement(); +}); + +namespace SelectArrow { + export interface OwnerState { + open: boolean; + side: 'top' | 'bottom' | 'left' | 'right' | 'none'; + alignment: 'start' | 'center' | 'end'; + arrowUncentered: boolean; + } + export interface Props extends BaseUIComponentProps<'div', OwnerState> { + /** + * If `true`, the arrow is hidden when it can't point to the center of the anchor element. + * @default false + */ + hideWhenUncentered?: boolean; + } +} + +SelectArrow.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * If `true`, the arrow is hidden when it can't point to the center of the anchor element. + * @default false + */ + hideWhenUncentered: PropTypes.bool, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectArrow }; diff --git a/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx b/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx new file mode 100644 index 000000000..8190313ed --- /dev/null +++ b/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLSpanElement, + render(node) { + return render( + + {node} + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/Icon/SelectIcon.tsx b/packages/mui-base/src/Select/Icon/SelectIcon.tsx new file mode 100644 index 000000000..09031defb --- /dev/null +++ b/packages/mui-base/src/Select/Icon/SelectIcon.tsx @@ -0,0 +1,59 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { mergeReactProps } from '../../utils/mergeReactProps'; + +const SelectIcon = React.forwardRef(function SelectIcon( + props: SelectIcon.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ...otherProps } = props; + + const ownerState: SelectIcon.OwnerState = React.useMemo(() => ({}), []); + + const getIconProps = React.useCallback((externalProps: React.ComponentProps<'span'>) => { + return mergeReactProps(externalProps, { + 'aria-hidden': true, + children: '▼', + }); + }, []); + + const { renderElement } = useComponentRenderer({ + propGetter: getIconProps, + render: render ?? 'span', + ref: forwardedRef, + className, + ownerState, + extraProps: otherProps, + }); + + return renderElement(); +}); + +namespace SelectIcon { + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} + +SelectIcon.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectIcon }; diff --git a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx index 992112fa0..803099625 100644 --- a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx @@ -7,6 +7,7 @@ import { commonStyleHooks } from '../utils/commonStyleHooks'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; import { useSelectOptionContext } from '../Option/SelectOptionContext'; +import { mergeReactProps } from '../../utils/mergeReactProps'; const customStyleHookMapping: CustomStyleHookMapping = commonStyleHooks; @@ -20,6 +21,14 @@ const SelectOptionIndicator = React.forwardRef(function SelectOptionIndicator( const { open } = useSelectRootContext(); const { selected } = useSelectOptionContext(); + const getOptionProps = React.useCallback( + (externalProps = {}) => + mergeReactProps(externalProps, { + 'aria-hidden': true, + }), + [], + ); + const ownerState: SelectOptionIndicator.OwnerState = React.useMemo( () => ({ open, @@ -29,6 +38,7 @@ const SelectOptionIndicator = React.forwardRef(function SelectOptionIndicator( ); const { renderElement } = useComponentRenderer({ + propGetter: getOptionProps, render: render ?? 'span', ref: forwardedRef, className, diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.tsx index cf9b6ec64..53e718259 100644 --- a/packages/mui-base/src/Select/Popup/SelectPopup.tsx +++ b/packages/mui-base/src/Select/Popup/SelectPopup.tsx @@ -69,7 +69,7 @@ namespace SelectPopup { export interface OwnerState { entering: boolean; exiting: boolean; - side: Side; + side: Side | 'none'; alignment: 'start' | 'end' | 'center'; open: boolean; } diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index de6ffe94f..dc1156a8d 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -272,11 +272,11 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( export { SelectPositioner }; export namespace SelectPositioner { - export type OwnerState = { + export interface OwnerState { open: boolean; - side: Side; + side: Side | 'none'; alignment: 'start' | 'end' | 'center'; - }; + } export interface Props extends useSelectPositioner.SharedParameters, diff --git a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts index 627ce4cc1..eab042161 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts +++ b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts @@ -6,7 +6,7 @@ export interface SelectPositionerContext { /** * The side of the anchor element the popup is positioned relative to. */ - side: Side; + side: Side | 'none'; /** * The alignment of the anchor element the popup is positioned relative to. */ diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 59302a38c..b0f2e92f1 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -78,7 +78,7 @@ export function useSelectPositioner( arrowRef, arrowUncentered, arrowStyles, - side: renderedSide, + side: alignMethod === 'item' && !innerFallback ? 'none' : renderedSide, alignment: renderedAlignment, floatingContext, isPositioned, @@ -88,6 +88,8 @@ export function useSelectPositioner( arrowRef, arrowUncentered, arrowStyles, + alignMethod, + innerFallback, renderedSide, renderedAlignment, floatingContext, @@ -228,7 +230,7 @@ export namespace useSelectPositioner { /** * The rendered side of the Select element. */ - side: 'top' | 'right' | 'bottom' | 'left'; + side: 'top' | 'right' | 'bottom' | 'left' | 'none'; /** * The rendered alignment of the Select element. */ diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index 303115725..7efac069e 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -17,15 +17,13 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( ) { const { render, className, direction, keepMounted = false, ...otherProps } = props; - const { innerOffset, setInnerOffset, innerFallback, alignMethod, popupRef, touchModality } = + const { innerOffset, setInnerOffset, innerFallback, popupRef, touchModality } = useSelectRootContext(); - const { isPositioned } = useSelectPositionerContext(); + const { isPositioned, side } = useSelectPositionerContext(); const [rendered, setRendered] = React.useState(false); - const inert = !(!touchModality && alignMethod && !innerFallback); - - if (rendered && inert) { + if (rendered && touchModality) { setRendered(false); } @@ -35,8 +33,9 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( () => ({ direction, rendered, + side, }), - [direction, rendered], + [direction, rendered, side], ); const getScrollArrowProps = React.useCallback( @@ -48,7 +47,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( zIndex: 2147483647, // max z-index }, onMouseEnter() { - if (inert) { + if (touchModality) { return; } @@ -89,7 +88,14 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( } const scrollDirection = direction === 'up' ? -1 : 1; - setInnerOffset((o) => o + scrollDirection * pixelsToScroll); + + if (innerFallback) { + setInnerOffset(0); + popupRef.current.scrollTop += scrollDirection * pixelsToScroll; + } else { + setInnerOffset((o) => o + scrollDirection * pixelsToScroll); + } + frameRef.current = requestAnimationFrame(handleFrame); } @@ -99,7 +105,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( cancelAnimationFrame(frameRef.current); }, }), - [direction, popupRef, setInnerOffset, inert], + [direction, innerFallback, popupRef, setInnerOffset, touchModality], ); const handleScrollArrowRendered = useEventCallback(() => { @@ -120,7 +126,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( React.useEffect(() => { const popupElement = popupRef.current; - if (!popupElement || inert) { + if (!popupElement || touchModality) { return undefined; } @@ -131,15 +137,15 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( popupElement.removeEventListener('wheel', handleScrollArrowRendered); popupElement.removeEventListener('scroll', handleScrollArrowRendered); }; - }, [inert, popupRef, direction, handleScrollArrowRendered]); + }, [touchModality, popupRef, direction, handleScrollArrowRendered]); useEnhancedEffect(() => { - if (!isPositioned || inert) { + if (!isPositioned || touchModality) { return; } handleScrollArrowRendered(); - }, [isPositioned, innerOffset, inert, handleScrollArrowRendered]); + }, [isPositioned, innerOffset, touchModality, handleScrollArrowRendered]); const { renderElement } = useComponentRenderer({ propGetter: getScrollArrowProps, @@ -161,6 +167,8 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( namespace SelectScrollArrow { export interface OwnerState { direction: 'up' | 'down'; + side: 'top' | 'right' | 'bottom' | 'left' | 'none'; + rendered: boolean; } export interface Props extends BaseUIComponentProps<'div', OwnerState> { direction: 'up' | 'down'; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index b9ae364e7..96326d6bb 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -11,3 +11,5 @@ export { SelectValue } from './Value/SelectValue'; export { SelectScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; export { SelectScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; export { SelectSeparator } from './Separator/SelectSeparator'; +export { SelectIcon } from './Icon/SelectIcon'; +export { SelectArrow } from './Arrow/SelectArrow'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 1c87eb63c..2edc671ec 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -11,3 +11,5 @@ export { SelectValue as Value } from './Value/SelectValue'; export { SelectScrollUpArrow as ScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; export { SelectSeparator as Separator } from './Separator/SelectSeparator'; +export { SelectIcon as Icon } from './Icon/SelectIcon'; +export { SelectArrow as Arrow } from './Arrow/SelectArrow'; From da16d71371f6eea2a7055d5227cd74d8b311c9ec Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 13:03:41 +1000 Subject: [PATCH 28/94] Codegen --- docs/data/api/number-field-root.json | 2 +- docs/data/base/pagesApi.js | 12 ++++++++++++ .../src/NumberField/Root/NumberFieldRoot.tsx | 9 ++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/data/api/number-field-root.json b/docs/data/api/number-field-root.json index 5b3e34aca..e512204e1 100644 --- a/docs/data/api/number-field-root.json +++ b/docs/data/api/number-field-root.json @@ -8,7 +8,7 @@ "format": { "type": { "name": "shape", - "description": "{ compactDisplay?: 'long'
        | 'short', currency?: string, currencyDisplay?: 'code'
        | 'name'
        | 'narrowSymbol'
        | 'symbol', currencySign?: 'accounting'
        | 'standard', localeMatcher?: 'best fit'
        | 'lookup', maximumFractionDigits?: number, maximumSignificantDigits?: number, minimumFractionDigits?: number, minimumIntegerDigits?: number, minimumSignificantDigits?: number, notation?: 'compact'
        | 'engineering'
        | 'scientific'
        | 'standard', numberingSystem?: string, signDisplay?: 'always'
        | 'auto'
        | 'exceptZero'
        | 'never', style?: 'currency'
        | 'decimal'
        | 'percent'
        | 'unit', unit?: string, unitDisplay?: 'long'
        | 'narrow'
        | 'short', useGrouping?: bool }" + "description": "{ compactDisplay?: 'long'
        | 'short', currency?: string, currencyDisplay?: string, currencySign?: string, localeMatcher?: string, maximumFractionDigits?: number, maximumSignificantDigits?: number, minimumFractionDigits?: number, minimumIntegerDigits?: number, minimumSignificantDigits?: number, notation?: 'compact'
        | 'engineering'
        | 'scientific'
        | 'standard', signDisplay?: 'always'
        | 'auto'
        | 'exceptZero'
        | 'never', style?: string, unit?: string, unitDisplay?: 'long'
        | 'narrow'
        | 'short', useGrouping?: bool }" } }, "id": { "type": { "name": "string" } }, diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index 34260360c..3dc51c6e1 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -224,6 +224,18 @@ module.exports = [ pathname: '/base-ui/react-progress/components-api/#progress-track', title: 'ProgressTrack', }, + { + pathname: '/base-ui/react-radio-group/components-api/#radio-group-root', + title: 'RadioGroupRoot', + }, + { + pathname: '/base-ui/react-radio-group/components-api/#radio-indicator', + title: 'RadioIndicator', + }, + { + pathname: '/base-ui/react-radio-group/components-api/#radio-root', + title: 'RadioRoot', + }, { pathname: '/base-ui/react-select/components-api/#select-arrow', title: 'SelectArrow', diff --git a/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx b/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx index 201004938..b541d4baf 100644 --- a/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx +++ b/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx @@ -173,18 +173,17 @@ NumberFieldRoot.propTypes /* remove-proptypes */ = { format: PropTypes.shape({ compactDisplay: PropTypes.oneOf(['long', 'short']), currency: PropTypes.string, - currencyDisplay: PropTypes.oneOf(['code', 'name', 'narrowSymbol', 'symbol']), - currencySign: PropTypes.oneOf(['accounting', 'standard']), - localeMatcher: PropTypes.oneOf(['best fit', 'lookup']), + currencyDisplay: PropTypes.string, + currencySign: PropTypes.string, + localeMatcher: PropTypes.string, maximumFractionDigits: PropTypes.number, maximumSignificantDigits: PropTypes.number, minimumFractionDigits: PropTypes.number, minimumIntegerDigits: PropTypes.number, minimumSignificantDigits: PropTypes.number, notation: PropTypes.oneOf(['compact', 'engineering', 'scientific', 'standard']), - numberingSystem: PropTypes.string, signDisplay: PropTypes.oneOf(['always', 'auto', 'exceptZero', 'never']), - style: PropTypes.oneOf(['currency', 'decimal', 'percent', 'unit']), + style: PropTypes.string, unit: PropTypes.string, unitDisplay: PropTypes.oneOf(['long', 'narrow', 'short']), useGrouping: PropTypes.bool, From 30cdc2df7e91ac08aa574467512652dd4534df76 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 13:11:19 +1000 Subject: [PATCH 29/94] Fix lint --- docs/data/api/number-field-root.json | 2 +- .../select/SelectIntroduction/system/index.js | 1 - .../SelectIntroduction/system/index.tsx | 1 - .../src/Field/Root/FieldRoot.test.tsx | 58 +++++++++++++++---- .../src/NumberField/Root/NumberFieldRoot.tsx | 9 +-- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/docs/data/api/number-field-root.json b/docs/data/api/number-field-root.json index e512204e1..5b3e34aca 100644 --- a/docs/data/api/number-field-root.json +++ b/docs/data/api/number-field-root.json @@ -8,7 +8,7 @@ "format": { "type": { "name": "shape", - "description": "{ compactDisplay?: 'long'
        | 'short', currency?: string, currencyDisplay?: string, currencySign?: string, localeMatcher?: string, maximumFractionDigits?: number, maximumSignificantDigits?: number, minimumFractionDigits?: number, minimumIntegerDigits?: number, minimumSignificantDigits?: number, notation?: 'compact'
        | 'engineering'
        | 'scientific'
        | 'standard', signDisplay?: 'always'
        | 'auto'
        | 'exceptZero'
        | 'never', style?: string, unit?: string, unitDisplay?: 'long'
        | 'narrow'
        | 'short', useGrouping?: bool }" + "description": "{ compactDisplay?: 'long'
        | 'short', currency?: string, currencyDisplay?: 'code'
        | 'name'
        | 'narrowSymbol'
        | 'symbol', currencySign?: 'accounting'
        | 'standard', localeMatcher?: 'best fit'
        | 'lookup', maximumFractionDigits?: number, maximumSignificantDigits?: number, minimumFractionDigits?: number, minimumIntegerDigits?: number, minimumSignificantDigits?: number, notation?: 'compact'
        | 'engineering'
        | 'scientific'
        | 'standard', numberingSystem?: string, signDisplay?: 'always'
        | 'auto'
        | 'exceptZero'
        | 'never', style?: 'currency'
        | 'decimal'
        | 'percent'
        | 'unit', unit?: string, unitDisplay?: 'long'
        | 'narrow'
        | 'short', useGrouping?: bool }" } }, "id": { "type": { "name": "string" } }, diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/base/components/select/SelectIntroduction/system/index.js index 8f1769352..e5bdf0acc 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.js +++ b/docs/data/base/components/select/SelectIntroduction/system/index.js @@ -159,7 +159,6 @@ const SelectOption = styled(Select.Option)` outline: 0; cursor: default; border-radius: 4px; - scroll-margin: 4px; user-select: none; display: flex; align-items: center; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/base/components/select/SelectIntroduction/system/index.tsx index 8f1769352..e5bdf0acc 100644 --- a/docs/data/base/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/base/components/select/SelectIntroduction/system/index.tsx @@ -159,7 +159,6 @@ const SelectOption = styled(Select.Option)` outline: 0; cursor: default; border-radius: 4px; - scroll-margin: 4px; user-select: none; display: flex; align-items: center; diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 35c6a8780..ac2cc5aaa 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -4,12 +4,9 @@ import * as Checkbox from '@base_ui/react/Checkbox'; import * as Switch from '@base_ui/react/Switch'; import * as NumberField from '@base_ui/react/NumberField'; import * as Slider from '@base_ui/react/Slider'; -<<<<<<< HEAD import * as RadioGroup from '@base_ui/react/RadioGroup'; import * as Radio from '@base_ui/react/Radio'; -======= import * as Select from '@base_ui/react/Select'; ->>>>>>> e667fe6d (Add Field integration) import userEvent from '@testing-library/user-event'; import { act, @@ -242,7 +239,6 @@ describe('', () => { expect(input).to.have.attribute('aria-invalid', 'true'); }); -<<<<<<< HEAD it('supports RadioGroup', () => { render( 'error'}> @@ -262,7 +258,8 @@ describe('', () => { fireEvent.blur(group); expect(group).to.have.attribute('aria-invalid', 'true'); -======= + }); + it('supports Select', async () => { render( 'error'}> @@ -283,7 +280,27 @@ describe('', () => { await flushMicrotasks(); expect(trigger).to.have.attribute('aria-invalid', 'true'); ->>>>>>> e667fe6d (Add Field integration) + }); + + it('supports RadioGroup', () => { + render( + 'error'}> + + One + Two + + + , + ); + + const group = screen.getByTestId('group'); + + expect(group).not.to.have.attribute('aria-invalid'); + + fireEvent.focus(group); + fireEvent.blur(group); + + expect(group).to.have.attribute('aria-invalid', 'true'); }); }); }); @@ -460,7 +477,6 @@ describe('', () => { expect(root).to.have.attribute('data-touched', 'true'); }); -<<<<<<< HEAD it('supports RadioGroup (click)', () => { render( @@ -503,7 +519,8 @@ describe('', () => { expect(group).to.have.attribute('data-touched', 'true'); expect(control).to.have.attribute('data-touched', 'true'); -======= + }); + it('supports Select', async () => { render( @@ -529,7 +546,6 @@ describe('', () => { await flushMicrotasks(); expect(trigger).to.have.attribute('data-touched', 'true'); ->>>>>>> e667fe6d (Add Field integration) }); }); @@ -641,7 +657,6 @@ describe('', () => { expect(root).to.have.attribute('data-dirty', 'true'); }); -<<<<<<< HEAD it('supports RadioGroup', () => { render( @@ -659,7 +674,8 @@ describe('', () => { fireEvent.click(screen.getByText('One')); expect(group).to.have.attribute('data-dirty', 'true'); -======= + }); + it('supports Select', async () => { render( @@ -696,7 +712,25 @@ describe('', () => { await flushMicrotasks(); expect(trigger).to.have.attribute('data-dirty', 'true'); ->>>>>>> e667fe6d (Add Field integration) + }); + + it('supports RadioGroup', () => { + render( + + + One + Two + + , + ); + + const group = screen.getByTestId('group'); + + expect(group).not.to.have.attribute('data-dirty'); + + fireEvent.click(screen.getByText('One')); + + expect(group).to.have.attribute('data-dirty', 'true'); }); }); }); diff --git a/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx b/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx index b541d4baf..201004938 100644 --- a/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx +++ b/packages/mui-base/src/NumberField/Root/NumberFieldRoot.tsx @@ -173,17 +173,18 @@ NumberFieldRoot.propTypes /* remove-proptypes */ = { format: PropTypes.shape({ compactDisplay: PropTypes.oneOf(['long', 'short']), currency: PropTypes.string, - currencyDisplay: PropTypes.string, - currencySign: PropTypes.string, - localeMatcher: PropTypes.string, + currencyDisplay: PropTypes.oneOf(['code', 'name', 'narrowSymbol', 'symbol']), + currencySign: PropTypes.oneOf(['accounting', 'standard']), + localeMatcher: PropTypes.oneOf(['best fit', 'lookup']), maximumFractionDigits: PropTypes.number, maximumSignificantDigits: PropTypes.number, minimumFractionDigits: PropTypes.number, minimumIntegerDigits: PropTypes.number, minimumSignificantDigits: PropTypes.number, notation: PropTypes.oneOf(['compact', 'engineering', 'scientific', 'standard']), + numberingSystem: PropTypes.string, signDisplay: PropTypes.oneOf(['always', 'auto', 'exceptZero', 'never']), - style: PropTypes.string, + style: PropTypes.oneOf(['currency', 'decimal', 'percent', 'unit']), unit: PropTypes.string, unitDisplay: PropTypes.oneOf(['long', 'narrow', 'short']), useGrouping: PropTypes.bool, From e6680014853b7da3a4edae416cefa4be427dfa0d Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 13:49:15 +1000 Subject: [PATCH 30/94] Update Field tests --- .../src/Field/Root/FieldRoot.test.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index ac2cc5aaa..962993948 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -8,16 +8,10 @@ import * as RadioGroup from '@base_ui/react/RadioGroup'; import * as Radio from '@base_ui/react/Radio'; import * as Select from '@base_ui/react/Select'; import userEvent from '@testing-library/user-event'; -import { - act, - createRenderer, - fireEvent, - flushMicrotasks, - screen, - waitFor, -} from '@mui/internal-test-utils'; +import { act, fireEvent, flushMicrotasks, screen, waitFor } from '@mui/internal-test-utils'; import { expect } from 'chai'; import { describeConformance } from '../../../test/describeConformance'; +import { createRenderer } from '../../../test/createRenderer'; const user = userEvent.setup(); @@ -215,8 +209,8 @@ describe('', () => { expect(input).to.have.attribute('aria-invalid', 'true'); }); - it('supports Slider', () => { - const { container } = render( + it('supports Slider', async () => { + const { container } = await render( 'error'}> @@ -635,8 +629,8 @@ describe('', () => { expect(input).to.have.attribute('data-dirty', 'true'); }); - it('supports Slider', () => { - const { container } = render( + it('supports Slider', async () => { + const { container } = await render( @@ -677,7 +671,7 @@ describe('', () => { }); it('supports Select', async () => { - render( + await render( From 3c55cc993771053e630461e95a0741c6a82e7165 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 14:06:59 +1000 Subject: [PATCH 31/94] Skip Select in JSDOM --- .../src/Field/Root/FieldRoot.test.tsx | 114 +++++++++++------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 962993948..c14eeac2e 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -46,8 +46,8 @@ describe('', () => { }); describe('prop: validate', () => { - it('should validate the field on blur', () => { - render( + it('should validate the field on blur', async () => { + await render( 'error'}> @@ -66,7 +66,7 @@ describe('', () => { }); it('supports async validation', async () => { - render( + await render( Promise.resolve('error')}> @@ -88,8 +88,8 @@ describe('', () => { }); }); - it('should apply [data-field] style hooks to field components', () => { - render( + it('should apply [data-field] style hooks to field components', async () => { + await render( Label Description @@ -134,8 +134,8 @@ describe('', () => { expect(error).to.equal(null); }); - it('should apply aria-invalid prop to control once validated', () => { - render( + it('should apply aria-invalid prop to control once validated', async () => { + await render( 'error'}> @@ -153,8 +153,8 @@ describe('', () => { }); describe('component integration', () => { - it('supports Checkbox', () => { - render( + it('supports Checkbox', async () => { + await render( 'error'}> @@ -171,8 +171,8 @@ describe('', () => { expect(button).to.have.attribute('aria-invalid', 'true'); }); - it('supports Switch', () => { - render( + it('supports Switch', async () => { + await render( 'error'}> @@ -189,8 +189,8 @@ describe('', () => { expect(button).to.have.attribute('aria-invalid', 'true'); }); - it('supports NumberField', () => { - render( + it('supports NumberField', async () => { + await render( 'error'}> @@ -255,7 +255,7 @@ describe('', () => { }); it('supports Select', async () => { - render( + await render( 'error'}> @@ -276,8 +276,8 @@ describe('', () => { expect(trigger).to.have.attribute('aria-invalid', 'true'); }); - it('supports RadioGroup', () => { - render( + it('supports RadioGroup', async () => { + await render( 'error'}> One @@ -301,7 +301,7 @@ describe('', () => { describe('prop: validateOnChange', () => { it('should validate the field on change', async () => { - render( + await render( { @@ -332,7 +332,7 @@ describe('', () => { clock.withFakeTimers(); it('should debounce validation', async () => { - renderFakeTimers( + await renderFakeTimers( ', () => { describe('style hooks', () => { describe('touched', () => { - it('should apply [data-touched] style hook to all components when touched', () => { - render( + it('should apply [data-touched] style hook to all components when touched', async () => { + await render( @@ -404,8 +404,8 @@ describe('', () => { expect(error).to.equal(null); }); - it('supports Checkbox', () => { - render( + it('supports Checkbox', async () => { + await render( , @@ -419,8 +419,8 @@ describe('', () => { expect(button).to.have.attribute('data-touched', 'true'); }); - it('supports Switch', () => { - render( + it('supports Switch', async () => { + await render( , @@ -434,8 +434,8 @@ describe('', () => { expect(button).to.have.attribute('data-touched', 'true'); }); - it('supports NumberField', () => { - render( + it('supports NumberField', async () => { + await render( @@ -451,8 +451,8 @@ describe('', () => { expect(input).to.have.attribute('data-touched', 'true'); }); - it('supports Slider', () => { - render( + it('supports Slider', async () => { + await render( @@ -471,8 +471,35 @@ describe('', () => { expect(root).to.have.attribute('data-touched', 'true'); }); - it('supports RadioGroup (click)', () => { - render( + it('supports Select', async () => { + await render( + + + + + + Select + Option 1 + + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + expect(trigger).not.to.have.attribute('data-dirty'); + + fireEvent.focus(trigger); + fireEvent.blur(trigger); + + await flushMicrotasks(); + + expect(trigger).to.have.attribute('data-touched', 'true'); + }); + + it('supports RadioGroup (click)', async () => { + await render( @@ -493,7 +520,7 @@ describe('', () => { }); it('supports RadioGroup (blur)', async () => { - render( + await render( @@ -544,8 +571,8 @@ describe('', () => { }); describe('dirty', () => { - it('should apply [data-dirty] style hook to all components when dirty', () => { - render( + it('should apply [data-dirty] style hook to all components when dirty', async () => { + await render( @@ -579,8 +606,8 @@ describe('', () => { expect(description).not.to.have.attribute('data-dirty'); }); - it('supports Checkbox', () => { - render( + it('supports Checkbox', async () => { + await render( , @@ -595,8 +622,8 @@ describe('', () => { expect(button).to.have.attribute('data-dirty', 'true'); }); - it('supports Switch', () => { - render( + it('supports Switch', async () => { + await render( , @@ -611,8 +638,8 @@ describe('', () => { expect(button).to.have.attribute('data-dirty', 'true'); }); - it('supports NumberField', () => { - render( + it('supports NumberField', async () => { + await render( @@ -671,6 +698,11 @@ describe('', () => { }); it('supports Select', async () => { + // ResizeObserver error + if (/jsdom/.test(window.navigator.userAgent)) { + return; + } + await render( @@ -708,8 +740,8 @@ describe('', () => { expect(trigger).to.have.attribute('data-dirty', 'true'); }); - it('supports RadioGroup', () => { - render( + it('supports RadioGroup', async () => { + await render( One From 6bb7adcc8f86e92289d66b63afea626b59e900cc Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 14:18:41 +1000 Subject: [PATCH 32/94] Fix tests --- packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx | 2 +- .../src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx | 2 +- .../src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx index 2c2b6077a..4edde331d 100644 --- a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx @@ -9,7 +9,7 @@ describe('', () => { refInstanceof: window.HTMLDivElement, render(node) { return render( - + {node} , ); diff --git a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx index 9c985753c..2638a763b 100644 --- a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx +++ b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx @@ -5,7 +5,7 @@ import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( diff --git a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx index 2134c2c4c..7eae77b05 100644 --- a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx +++ b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx @@ -5,7 +5,7 @@ import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( From 8fc81d21ef0e6d6762477950ae7dfbbd22684b98 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 14:40:16 +1000 Subject: [PATCH 33/94] Add ResizeObserver mock --- packages/mui-base/src/Field/Root/FieldRoot.test.tsx | 7 ++----- packages/mui-base/test/setup.ts | 6 ++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index c14eeac2e..04c34d335 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -13,6 +13,8 @@ import { expect } from 'chai'; import { describeConformance } from '../../../test/describeConformance'; import { createRenderer } from '../../../test/createRenderer'; +console.log(typeof ResizeObserver); + const user = userEvent.setup(); describe('', () => { @@ -698,11 +700,6 @@ describe('', () => { }); it('supports Select', async () => { - // ResizeObserver error - if (/jsdom/.test(window.navigator.userAgent)) { - return; - } - await render( diff --git a/packages/mui-base/test/setup.ts b/packages/mui-base/test/setup.ts index dcb09b3db..8f7602bc1 100644 --- a/packages/mui-base/test/setup.ts +++ b/packages/mui-base/test/setup.ts @@ -2,3 +2,9 @@ globalThis.requestAnimationFrame = (cb) => { cb(0); return 0; }; + +globalThis.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} +}; From b1d0abffb6a6b96cc63d60a9cf33d70e51e12f51 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 14:41:21 +1000 Subject: [PATCH 34/94] Remove log --- packages/mui-base/src/Field/Root/FieldRoot.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 04c34d335..3b368df44 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -13,8 +13,6 @@ import { expect } from 'chai'; import { describeConformance } from '../../../test/describeConformance'; import { createRenderer } from '../../../test/createRenderer'; -console.log(typeof ResizeObserver); - const user = userEvent.setup(); describe('', () => { From 5e0e019f11329833f4ce31002290f59ede96f9e6 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 14:58:38 +1000 Subject: [PATCH 35/94] Fix alignMethod checks --- .../src/Select/Positioner/SelectPositioner.tsx | 2 +- .../Select/Positioner/useSelectPositioner.tsx | 2 +- .../Select/ScrollArrow/SelectScrollArrow.tsx | 18 ++++++++++-------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index dc1156a8d..0d03564f3 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -129,7 +129,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( allowAxisFlip: false, innerFallback, inner: - alignMethod && selectedIndexOnMount !== null + alignMethod === 'item' && selectedIndexOnMount !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index b0f2e92f1..5427c0c73 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -27,7 +27,7 @@ export function useSelectPositioner( const { touchModality, alignMethod, innerFallback, mounted } = useSelectRootContext(); - useScrollLock(alignMethod && !innerFallback && mounted); + useScrollLock(alignMethod === 'item' && !innerFallback && mounted); const { positionerStyles, diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index 7efac069e..4ce820c07 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -17,13 +17,15 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( ) { const { render, className, direction, keepMounted = false, ...otherProps } = props; - const { innerOffset, setInnerOffset, innerFallback, popupRef, touchModality } = + const { alignMethod, innerOffset, setInnerOffset, innerFallback, popupRef, touchModality } = useSelectRootContext(); const { isPositioned, side } = useSelectPositionerContext(); const [rendered, setRendered] = React.useState(false); - if (rendered && touchModality) { + const inert = alignMethod === 'trigger' || touchModality; + + if (rendered && inert) { setRendered(false); } @@ -47,7 +49,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( zIndex: 2147483647, // max z-index }, onMouseEnter() { - if (touchModality) { + if (inert) { return; } @@ -105,7 +107,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( cancelAnimationFrame(frameRef.current); }, }), - [direction, innerFallback, popupRef, setInnerOffset, touchModality], + [direction, innerFallback, popupRef, setInnerOffset, inert], ); const handleScrollArrowRendered = useEventCallback(() => { @@ -126,7 +128,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( React.useEffect(() => { const popupElement = popupRef.current; - if (!popupElement || touchModality) { + if (!popupElement || inert) { return undefined; } @@ -137,15 +139,15 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( popupElement.removeEventListener('wheel', handleScrollArrowRendered); popupElement.removeEventListener('scroll', handleScrollArrowRendered); }; - }, [touchModality, popupRef, direction, handleScrollArrowRendered]); + }, [inert, popupRef, direction, handleScrollArrowRendered]); useEnhancedEffect(() => { - if (!isPositioned || touchModality) { + if (!isPositioned || inert) { return; } handleScrollArrowRendered(); - }, [isPositioned, innerOffset, touchModality, handleScrollArrowRendered]); + }, [isPositioned, innerOffset, inert, handleScrollArrowRendered]); const { renderElement } = useComponentRenderer({ propGetter: getScrollArrowProps, From e367c596dd7094027ecc5216c7793643c300183c Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 17:53:40 +1000 Subject: [PATCH 36/94] Fix tests and lint --- packages/mui-base/src/Field/Root/FieldRoot.test.tsx | 3 --- packages/mui-base/test/setup.ts | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 3b368df44..65f811db8 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -725,9 +725,6 @@ describe('', () => { // Arrow Down to focus the Option 1 await user.keyboard('{ArrowDown}'); - // Allow selection to take place by entering keyboard modality - fireEvent.keyDown(option, { key: '' }); - fireEvent.click(option); await flushMicrotasks(); diff --git a/packages/mui-base/test/setup.ts b/packages/mui-base/test/setup.ts index 8f7602bc1..8394fc6ef 100644 --- a/packages/mui-base/test/setup.ts +++ b/packages/mui-base/test/setup.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ + globalThis.requestAnimationFrame = (cb) => { cb(0); return 0; From 8c21e76754178d7aa5a359b7af28ca5399515a05 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 18:21:11 +1000 Subject: [PATCH 37/94] Fix ResizeObserver/IntersectionObserver in JSDOM --- packages/mui-base/src/utils/useAnchorPositioning.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index 1f121574c..39ee87c08 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -231,8 +231,8 @@ export function useAnchorPositioning( // Keep `ancestorResize` for window resizing. TODO: determine the best configuration, or // if we need to allow options. ancestorScroll: trackAnchor, - elementResize: trackAnchor, - layoutShift: trackAnchor, + elementResize: trackAnchor && typeof ResizeObserver !== 'undefined', + layoutShift: trackAnchor && typeof IntersectionObserver !== 'undefined', }), nodeId, }); From b253615674efda50429eb998332bedcaf4e8997c Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 18:21:31 +1000 Subject: [PATCH 38/94] Remove mock --- packages/mui-base/test/setup.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/mui-base/test/setup.ts b/packages/mui-base/test/setup.ts index 8394fc6ef..4f30b3934 100644 --- a/packages/mui-base/test/setup.ts +++ b/packages/mui-base/test/setup.ts @@ -4,9 +4,3 @@ globalThis.requestAnimationFrame = (cb) => { cb(0); return 0; }; - -globalThis.ResizeObserver = class { - observe() {} - unobserve() {} - disconnect() {} -}; From 8be2400d57a6da33e8545d63a02d8ef6d1bf2056 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 5 Sep 2024 18:26:52 +1000 Subject: [PATCH 39/94] Remove eslint-disable --- packages/mui-base/test/setup.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/mui-base/test/setup.ts b/packages/mui-base/test/setup.ts index 4f30b3934..dcb09b3db 100644 --- a/packages/mui-base/test/setup.ts +++ b/packages/mui-base/test/setup.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ - globalThis.requestAnimationFrame = (cb) => { cb(0); return 0; From a7f23dd81217908621c1bf77ad1b846a3c83ebaa Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 10:52:46 +1000 Subject: [PATCH 40/94] Codegen --- docs/data/api/menu-trigger.json | 6 - .../base-ui => data}/api/select-arrow.json | 2 +- .../base-ui => data}/api/select-backdrop.json | 2 +- .../base-ui => data}/api/select-icon.json | 2 +- .../api/select-option-group-label.json | 2 +- .../api/select-option-group.json | 2 +- .../api/select-option-indicator.json | 2 +- .../base-ui => data}/api/select-option.json | 2 +- .../base-ui => data}/api/select-popup.json | 2 +- .../api/select-positioner.json | 2 +- .../base-ui => data}/api/select-root.json | 2 +- .../api/select-scroll-down-arrow.json | 2 +- .../api/select-scroll-up-arrow.json | 2 +- .../api/select-separator.json | 2 +- docs/data/api/select-trigger.json | 22 + .../base-ui => data}/api/select-value.json | 2 +- docs/data/base/pages.ts | 93 ---- docs/data/base/pagesApi.js | 455 ------------------ .../select/SelectIntroduction/system/index.js | 0 .../SelectIntroduction/system/index.tsx | 0 .../{base => }/components/select/select.md | 0 .../api-docs/select-arrow/select-arrow.json | 13 + .../select-backdrop/select-backdrop.json | 14 + .../api-docs/select-icon/select-icon.json | 10 + .../select-option-group-label.json | 10 + .../select-option-group.json | 10 + .../select-option-indicator.json | 13 + .../api-docs/select-option/select-option.json | 12 + .../api-docs/select-popup/select-popup.json | 11 + .../select-positioner/select-positioner.json | 43 ++ .../api-docs/select-root/select-root.json | 29 ++ .../select-scroll-down-arrow.json | 9 + .../select-scroll-up-arrow.json | 9 + .../select-separator/select-separator.json | 10 + .../select-trigger/select-trigger.json | 15 + .../api-docs/select-value/select-value.json | 9 + .../base-ui/api/use-select-backdrop.json | 8 - docs/pages/base-ui/api/use-select-option.json | 8 - .../mui-base/src/Select/Arrow/SelectArrow.tsx | 11 +- .../src/Select/Backdrop/SelectBackdrop.tsx | 11 +- .../mui-base/src/Select/Icon/SelectIcon.tsx | 11 +- .../src/Select/Option/SelectOption.tsx | 4 +- .../Select/OptionGroup/SelectOptionGroup.tsx | 11 +- .../SelectOptionGroupLabel.tsx | 11 +- .../OptionIndicator/SelectOptionIndicator.tsx | 11 +- .../mui-base/src/Select/Popup/SelectPopup.tsx | 11 +- .../Select/Positioner/SelectPositioner.tsx | 4 +- .../mui-base/src/Select/Root/SelectRoot.tsx | 11 +- .../ScrollDownArrow/SelectScrollDownArrow.tsx | 11 +- .../ScrollUpArrow/SelectScrollUpArrow.tsx | 11 +- .../src/Select/Separator/SelectSeparator.tsx | 11 +- .../src/Select/Trigger/SelectTrigger.tsx | 11 +- .../mui-base/src/Select/Value/SelectValue.tsx | 13 +- 53 files changed, 388 insertions(+), 602 deletions(-) rename docs/{pages/base-ui => data}/api/select-arrow.json (89%) rename docs/{pages/base-ui => data}/api/select-backdrop.json (90%) rename docs/{pages/base-ui => data}/api/select-icon.json (87%) rename docs/{pages/base-ui => data}/api/select-option-group-label.json (88%) rename docs/{pages/base-ui => data}/api/select-option-group.json (88%) rename docs/{pages/base-ui => data}/api/select-option-indicator.json (89%) rename docs/{pages/base-ui => data}/api/select-option.json (88%) rename docs/{pages/base-ui => data}/api/select-popup.json (88%) rename docs/{pages/base-ui => data}/api/select-positioner.json (96%) rename docs/{pages/base-ui => data}/api/select-root.json (93%) rename docs/{pages/base-ui => data}/api/select-scroll-down-arrow.json (86%) rename docs/{pages/base-ui => data}/api/select-scroll-up-arrow.json (86%) rename docs/{pages/base-ui => data}/api/select-separator.json (88%) create mode 100644 docs/data/api/select-trigger.json rename docs/{pages/base-ui => data}/api/select-value.json (81%) delete mode 100644 docs/data/base/pages.ts delete mode 100644 docs/data/base/pagesApi.js rename docs/data/{base => }/components/select/SelectIntroduction/system/index.js (100%) rename docs/data/{base => }/components/select/SelectIntroduction/system/index.tsx (100%) rename docs/data/{base => }/components/select/select.md (100%) create mode 100644 docs/data/translations/api-docs/select-arrow/select-arrow.json create mode 100644 docs/data/translations/api-docs/select-backdrop/select-backdrop.json create mode 100644 docs/data/translations/api-docs/select-icon/select-icon.json create mode 100644 docs/data/translations/api-docs/select-option-group-label/select-option-group-label.json create mode 100644 docs/data/translations/api-docs/select-option-group/select-option-group.json create mode 100644 docs/data/translations/api-docs/select-option-indicator/select-option-indicator.json create mode 100644 docs/data/translations/api-docs/select-option/select-option.json create mode 100644 docs/data/translations/api-docs/select-popup/select-popup.json create mode 100644 docs/data/translations/api-docs/select-positioner/select-positioner.json create mode 100644 docs/data/translations/api-docs/select-root/select-root.json create mode 100644 docs/data/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json create mode 100644 docs/data/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json create mode 100644 docs/data/translations/api-docs/select-separator/select-separator.json create mode 100644 docs/data/translations/api-docs/select-trigger/select-trigger.json create mode 100644 docs/data/translations/api-docs/select-value/select-value.json delete mode 100644 docs/pages/base-ui/api/use-select-backdrop.json delete mode 100644 docs/pages/base-ui/api/use-select-option.json diff --git a/docs/data/api/menu-trigger.json b/docs/data/api/menu-trigger.json index b2991b86a..05e73a2a8 100644 --- a/docs/data/api/menu-trigger.json +++ b/docs/data/api/menu-trigger.json @@ -11,15 +11,9 @@ "classes": [], "spread": true, "themeDefaultProps": true, -<<<<<<< HEAD:docs/data/api/menu-trigger.json "muiName": "MenuTrigger", "forwardsRefTo": "HTMLButtonElement", "filename": "/packages/mui-base/src/Menu/Trigger/MenuTrigger.tsx", -======= - "muiName": "SelectTrigger", - "forwardsRefTo": "HTMLDivElement", - "filename": "/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx", ->>>>>>> 24117833 (Update Select.Trigger instanceof test):docs/pages/base-ui/api/select-trigger.json "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/select-arrow.json b/docs/data/api/select-arrow.json similarity index 89% rename from docs/pages/base-ui/api/select-arrow.json rename to docs/data/api/select-arrow.json index 1cf176708..9d3f1a6d7 100644 --- a/docs/pages/base-ui/api/select-arrow.json +++ b/docs/data/api/select-arrow.json @@ -15,6 +15,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Arrow/SelectArrow.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-backdrop.json b/docs/data/api/select-backdrop.json similarity index 90% rename from docs/pages/base-ui/api/select-backdrop.json rename to docs/data/api/select-backdrop.json index ca4f23bb3..2138d5222 100644 --- a/docs/pages/base-ui/api/select-backdrop.json +++ b/docs/data/api/select-backdrop.json @@ -19,6 +19,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-icon.json b/docs/data/api/select-icon.json similarity index 87% rename from docs/pages/base-ui/api/select-icon.json rename to docs/data/api/select-icon.json index f3ac336a3..9ddfe9fdb 100644 --- a/docs/pages/base-ui/api/select-icon.json +++ b/docs/data/api/select-icon.json @@ -12,6 +12,6 @@ "forwardsRefTo": "HTMLSpanElement", "filename": "/packages/mui-base/src/Select/Icon/SelectIcon.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-option-group-label.json b/docs/data/api/select-option-group-label.json similarity index 88% rename from docs/pages/base-ui/api/select-option-group-label.json rename to docs/data/api/select-option-group-label.json index 910414040..d975856fb 100644 --- a/docs/pages/base-ui/api/select-option-group-label.json +++ b/docs/data/api/select-option-group-label.json @@ -14,6 +14,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-option-group.json b/docs/data/api/select-option-group.json similarity index 88% rename from docs/pages/base-ui/api/select-option-group.json rename to docs/data/api/select-option-group.json index 9f5003716..81b819ef9 100644 --- a/docs/pages/base-ui/api/select-option-group.json +++ b/docs/data/api/select-option-group.json @@ -14,6 +14,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-option-indicator.json b/docs/data/api/select-option-indicator.json similarity index 89% rename from docs/pages/base-ui/api/select-option-indicator.json rename to docs/data/api/select-option-indicator.json index e40564a3a..1dbd62192 100644 --- a/docs/pages/base-ui/api/select-option-indicator.json +++ b/docs/data/api/select-option-indicator.json @@ -15,6 +15,6 @@ "forwardsRefTo": "HTMLSpanElement", "filename": "/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-option.json b/docs/data/api/select-option.json similarity index 88% rename from docs/pages/base-ui/api/select-option.json rename to docs/data/api/select-option.json index aa008d15a..7bf8537d2 100644 --- a/docs/pages/base-ui/api/select-option.json +++ b/docs/data/api/select-option.json @@ -16,6 +16,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Option/SelectOption.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-popup.json b/docs/data/api/select-popup.json similarity index 88% rename from docs/pages/base-ui/api/select-popup.json rename to docs/data/api/select-popup.json index 4d86fe4bf..309e0d76c 100644 --- a/docs/pages/base-ui/api/select-popup.json +++ b/docs/data/api/select-popup.json @@ -15,6 +15,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Popup/SelectPopup.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-positioner.json b/docs/data/api/select-positioner.json similarity index 96% rename from docs/pages/base-ui/api/select-positioner.json rename to docs/data/api/select-positioner.json index adf7fc574..36d0d520d 100644 --- a/docs/pages/base-ui/api/select-positioner.json +++ b/docs/data/api/select-positioner.json @@ -59,6 +59,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-root.json b/docs/data/api/select-root.json similarity index 93% rename from docs/pages/base-ui/api/select-root.json rename to docs/data/api/select-root.json index 09074d827..c6b67da92 100644 --- a/docs/pages/base-ui/api/select-root.json +++ b/docs/data/api/select-root.json @@ -25,6 +25,6 @@ "muiName": "SelectRoot", "filename": "/packages/mui-base/src/Select/Root/SelectRoot.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-scroll-down-arrow.json b/docs/data/api/select-scroll-down-arrow.json similarity index 86% rename from docs/pages/base-ui/api/select-scroll-down-arrow.json rename to docs/data/api/select-scroll-down-arrow.json index 267439cf9..18e40893d 100644 --- a/docs/pages/base-ui/api/select-scroll-down-arrow.json +++ b/docs/data/api/select-scroll-down-arrow.json @@ -11,6 +11,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-scroll-up-arrow.json b/docs/data/api/select-scroll-up-arrow.json similarity index 86% rename from docs/pages/base-ui/api/select-scroll-up-arrow.json rename to docs/data/api/select-scroll-up-arrow.json index ad5f0d8e5..d0caea010 100644 --- a/docs/pages/base-ui/api/select-scroll-up-arrow.json +++ b/docs/data/api/select-scroll-up-arrow.json @@ -11,6 +11,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/pages/base-ui/api/select-separator.json b/docs/data/api/select-separator.json similarity index 88% rename from docs/pages/base-ui/api/select-separator.json rename to docs/data/api/select-separator.json index 20b7af4f3..2f691f9d6 100644 --- a/docs/pages/base-ui/api/select-separator.json +++ b/docs/data/api/select-separator.json @@ -14,6 +14,6 @@ "forwardsRefTo": "HTMLDivElement", "filename": "/packages/mui-base/src/Select/Separator/SelectSeparator.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/data/api/select-trigger.json b/docs/data/api/select-trigger.json new file mode 100644 index 000000000..16364da74 --- /dev/null +++ b/docs/data/api/select-trigger.json @@ -0,0 +1,22 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
        | string" } }, + "disabled": { "type": { "name": "bool" }, "default": "false" }, + "focusableWhenDisabled": { "type": { "name": "bool" }, "default": "false" }, + "label": { "type": { "name": "string" } }, + "render": { "type": { "name": "union", "description": "element
        | func" } } + }, + "name": "SelectTrigger", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectTrigger = Select.Trigger;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectTrigger", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/select-value.json b/docs/data/api/select-value.json similarity index 81% rename from docs/pages/base-ui/api/select-value.json rename to docs/data/api/select-value.json index 1c1f9e588..72d826363 100644 --- a/docs/pages/base-ui/api/select-value.json +++ b/docs/data/api/select-value.json @@ -8,6 +8,6 @@ "muiName": "SelectValue", "filename": "/packages/mui-base/src/Select/Value/SelectValue.tsx", "inheritance": null, - "demos": "", + "demos": "", "cssComponent": false } diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts deleted file mode 100644 index 758d6f08a..000000000 --- a/docs/data/base/pages.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { MuiPage } from 'docs/src/MuiPage'; - -const pages: readonly MuiPage[] = [ - { - pathname: '/base-ui/getting-started-group', - title: 'Getting started', - children: [ - { pathname: '/base-ui/getting-started', title: 'Overview' }, - { pathname: '/base-ui/getting-started/quickstart', title: 'Quickstart' }, - { pathname: '/base-ui/getting-started/usage', title: 'Usage' }, - { pathname: '/base-ui/getting-started/support' }, - ], - }, - { - pathname: '/base-ui/react-', - title: 'Components', - children: [ - { pathname: '/base-ui/all-components', title: 'All components' }, - { - pathname: '/base-ui/components/inputs', - subheader: 'inputs', - children: [ - // { pathname: '/base-ui/react-autocomplete', title: 'Autocomplete' }, - { pathname: '/base-ui/react-checkbox', title: 'Checkbox' }, - { pathname: '/base-ui/react-number-field', title: 'Number Field' }, - // { pathname: '/base-ui/react-radio-group', title: 'Radio Group', planned: true }, - { pathname: '/base-ui/react-select', title: 'Select' }, - { pathname: '/base-ui/react-slider', title: 'Slider' }, - { pathname: '/base-ui/react-switch', title: 'Switch' }, - ], - }, - { - pathname: '/base-ui/components/data-display', - subheader: 'data-display', - children: [ - { pathname: '/base-ui/react-popover', title: 'Popover' }, - { pathname: '/base-ui/react-preview-card', title: 'Preview Card' }, - { pathname: '/base-ui/react-tooltip', title: 'Tooltip' }, - { pathname: '/base-ui/react-field', title: 'Field' }, - { pathname: '/base-ui/react-fieldset', title: 'Fieldset' }, - ], - }, - { - pathname: '/base-ui/components/feedback', - subheader: 'feedback', - children: [ - { pathname: '/base-ui/react-alert-dialog', title: 'Alert Dialog' }, - { pathname: '/base-ui/react-dialog', title: 'Dialog' }, - { pathname: '/base-ui/react-progress', title: 'Progress' }, - ], - }, - { - pathname: '/base-ui/components/navigation', - subheader: 'navigation', - children: [ - { pathname: '/base-ui/react-menu', title: 'Menu' }, - // { pathname: '/base-ui/react-table-pagination', title: 'Table Pagination' }, - { pathname: '/base-ui/react-tabs', title: 'Tabs' }, - ], - }, - // { - // pathname: '/base-ui/components/utils', - // subheader: 'utils', - // children: [ - // { pathname: '/base-ui/react-click-away-listener', title: 'Click-Away Listener' }, - // { pathname: '/base-ui/react-focus-trap', title: 'Focus Trap' }, - // { pathname: '/base-ui/react-form-control', title: 'Form Control' }, - // { pathname: '/base-ui/react-no-ssr', title: 'No-SSR' }, - // { pathname: '/base-ui/react-popup', title: 'Popup', unstable: true }, - // { pathname: '/base-ui/react-portal', title: 'Portal' }, - // { pathname: '/base-ui/react-textarea-autosize', title: 'Textarea Autosize' }, - // ], - // }, - ], - }, - /* { - title: 'APIs', - pathname: '/base-ui/api', - children: pagesApi, - }, */ - { - pathname: '/base-ui/guides', - title: 'How-to guides', - children: [ - { - pathname: '/base-ui/guides/next-js-app-router', - title: 'Next.js App Router', - }, - ], - }, -]; - -export default pages; diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js deleted file mode 100644 index 3dc51c6e1..000000000 --- a/docs/data/base/pagesApi.js +++ /dev/null @@ -1,455 +0,0 @@ -module.exports = [ - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-backdrop', - title: 'AlertDialogBackdrop', - }, - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-close', - title: 'AlertDialogClose', - }, - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-description', - title: 'AlertDialogDescription', - }, - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-popup', - title: 'AlertDialogPopup', - }, - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-root', - title: 'AlertDialogRoot', - }, - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-title', - title: 'AlertDialogTitle', - }, - { - pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-trigger', - title: 'AlertDialogTrigger', - }, - { - pathname: '/base-ui/react-checkbox/components-api/#checkbox-indicator', - title: 'CheckboxIndicator', - }, - { - pathname: '/base-ui/react-checkbox/components-api/#checkbox-root', - title: 'CheckboxRoot', - }, - { - pathname: - '/base-ui/react-click-away-listener/components-api/#click-away-listener', - title: 'ClickAwayListener', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-backdrop', - title: 'DialogBackdrop', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-close', - title: 'DialogClose', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-description', - title: 'DialogDescription', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-popup', - title: 'DialogPopup', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-root', - title: 'DialogRoot', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-title', - title: 'DialogTitle', - }, - { - pathname: '/base-ui/react-dialog/components-api/#dialog-trigger', - title: 'DialogTrigger', - }, - { - pathname: '/base-ui/react-field/components-api/#field-control', - title: 'FieldControl', - }, - { - pathname: '/base-ui/react-field/components-api/#field-description', - title: 'FieldDescription', - }, - { - pathname: '/base-ui/react-field/components-api/#field-error', - title: 'FieldError', - }, - { - pathname: '/base-ui/react-field/components-api/#field-label', - title: 'FieldLabel', - }, - { - pathname: '/base-ui/react-field/components-api/#field-root', - title: 'FieldRoot', - }, - { - pathname: '/base-ui/react-field/components-api/#field-validity', - title: 'FieldValidity', - }, - { - pathname: '/base-ui/react-fieldset/components-api/#fieldset-legend', - title: 'FieldsetLegend', - }, - { - pathname: '/base-ui/react-fieldset/components-api/#fieldset-root', - title: 'FieldsetRoot', - }, - { - pathname: '/base-ui/react-focus-trap/components-api/#focus-trap', - title: 'FocusTrap', - }, - { - pathname: '/base-ui/react-form-control/components-api/#form-control', - title: 'FormControl', - }, - { pathname: '/base-ui/react-menu/components-api/#menu-arrow', title: 'MenuArrow' }, - { pathname: '/base-ui/react-menu/components-api/#menu-item', title: 'MenuItem' }, - { pathname: '/base-ui/react-menu/components-api/#menu-popup', title: 'MenuPopup' }, - { - pathname: '/base-ui/react-menu/components-api/#menu-positioner', - title: 'MenuPositioner', - }, - { pathname: '/base-ui/react-menu/components-api/#menu-root', title: 'MenuRoot' }, - { - pathname: '/base-ui/react-menu/components-api/#menu-trigger', - title: 'MenuTrigger', - }, - { pathname: '/base-ui/react-no-ssr/components-api/#no-ssr', title: 'NoSsr' }, - { - pathname: '/base-ui/react-number-field/components-api/#number-field-decrement', - title: 'NumberFieldDecrement', - }, - { - pathname: '/base-ui/react-number-field/components-api/#number-field-group', - title: 'NumberFieldGroup', - }, - { - pathname: '/base-ui/react-number-field/components-api/#number-field-increment', - title: 'NumberFieldIncrement', - }, - { - pathname: '/base-ui/react-number-field/components-api/#number-field-input', - title: 'NumberFieldInput', - }, - { - pathname: '/base-ui/react-number-field/components-api/#number-field-root', - title: 'NumberFieldRoot', - }, - { - pathname: '/base-ui/react-number-field/components-api/#number-field-scrub-area', - title: 'NumberFieldScrubArea', - }, - { - pathname: - '/base-ui/react-number-field/components-api/#number-field-scrub-area-cursor', - title: 'NumberFieldScrubAreaCursor', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-arrow', - title: 'PopoverArrow', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-backdrop', - title: 'PopoverBackdrop', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-close', - title: 'PopoverClose', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-description', - title: 'PopoverDescription', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-popup', - title: 'PopoverPopup', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-positioner', - title: 'PopoverPositioner', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-root', - title: 'PopoverRoot', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-title', - title: 'PopoverTitle', - }, - { - pathname: '/base-ui/react-popover/components-api/#popover-trigger', - title: 'PopoverTrigger', - }, - { pathname: '/base-ui/react-popup/components-api/#popup', title: 'Popup' }, - { pathname: '/base-ui/react-portal/components-api/#portal', title: 'Portal' }, - { - pathname: '/base-ui/react-preview-card/components-api/#preview-card-arrow', - title: 'PreviewCardArrow', - }, - { - pathname: '/base-ui/react-preview-card/components-api/#preview-card-backdrop', - title: 'PreviewCardBackdrop', - }, - { - pathname: '/base-ui/react-preview-card/components-api/#preview-card-popup', - title: 'PreviewCardPopup', - }, - { - pathname: '/base-ui/react-preview-card/components-api/#preview-card-positioner', - title: 'PreviewCardPositioner', - }, - { - pathname: '/base-ui/react-preview-card/components-api/#preview-card-root', - title: 'PreviewCardRoot', - }, - { - pathname: '/base-ui/react-preview-card/components-api/#preview-card-trigger', - title: 'PreviewCardTrigger', - }, - { - pathname: '/base-ui/react-progress/components-api/#progress-indicator', - title: 'ProgressIndicator', - }, - { - pathname: '/base-ui/react-progress/components-api/#progress-root', - title: 'ProgressRoot', - }, - { - pathname: '/base-ui/react-progress/components-api/#progress-track', - title: 'ProgressTrack', - }, - { - pathname: '/base-ui/react-radio-group/components-api/#radio-group-root', - title: 'RadioGroupRoot', - }, - { - pathname: '/base-ui/react-radio-group/components-api/#radio-indicator', - title: 'RadioIndicator', - }, - { - pathname: '/base-ui/react-radio-group/components-api/#radio-root', - title: 'RadioRoot', - }, - { - pathname: '/base-ui/react-select/components-api/#select-arrow', - title: 'SelectArrow', - }, - { - pathname: '/base-ui/react-select/components-api/#select-backdrop', - title: 'SelectBackdrop', - }, - { - pathname: '/base-ui/react-select/components-api/#select-icon', - title: 'SelectIcon', - }, - { - pathname: '/base-ui/react-select/components-api/#select-option', - title: 'SelectOption', - }, - { - pathname: '/base-ui/react-select/components-api/#select-option-group', - title: 'SelectOptionGroup', - }, - { - pathname: '/base-ui/react-select/components-api/#select-option-group-label', - title: 'SelectOptionGroupLabel', - }, - { - pathname: '/base-ui/react-select/components-api/#select-option-indicator', - title: 'SelectOptionIndicator', - }, - { - pathname: '/base-ui/react-select/components-api/#select-popup', - title: 'SelectPopup', - }, - { - pathname: '/base-ui/react-select/components-api/#select-positioner', - title: 'SelectPositioner', - }, - { - pathname: '/base-ui/react-select/components-api/#select-root', - title: 'SelectRoot', - }, - { - pathname: '/base-ui/react-select/components-api/#select-scroll-down-arrow', - title: 'SelectScrollDownArrow', - }, - { - pathname: '/base-ui/react-select/components-api/#select-scroll-up-arrow', - title: 'SelectScrollUpArrow', - }, - { - pathname: '/base-ui/react-select/components-api/#select-separator', - title: 'SelectSeparator', - }, - { - pathname: '/base-ui/react-select/components-api/#select-trigger', - title: 'SelectTrigger', - }, - { - pathname: '/base-ui/react-select/components-api/#select-value', - title: 'SelectValue', - }, - { - pathname: '/base-ui/react-slider/components-api/#slider-control', - title: 'SliderControl', - }, - { - pathname: '/base-ui/react-slider/components-api/#slider-indicator', - title: 'SliderIndicator', - }, - { - pathname: '/base-ui/react-slider/components-api/#slider-output', - title: 'SliderOutput', - }, - { - pathname: '/base-ui/react-slider/components-api/#slider-root', - title: 'SliderRoot', - }, - { - pathname: '/base-ui/react-slider/components-api/#slider-thumb', - title: 'SliderThumb', - }, - { - pathname: '/base-ui/react-slider/components-api/#slider-track', - title: 'SliderTrack', - }, - { - pathname: '/base-ui/react-snackbar/components-api/#snackbar', - title: 'Snackbar', - }, - { - pathname: '/base-ui/react-menu/components-api/#submenu-trigger', - title: 'SubmenuTrigger', - }, - { - pathname: '/base-ui/react-switch/components-api/#switch-root', - title: 'SwitchRoot', - }, - { - pathname: '/base-ui/react-switch/components-api/#switch-thumb', - title: 'SwitchThumb', - }, - { pathname: '/base-ui/react-tabs/components-api/#tab', title: 'Tab' }, - { - pathname: '/base-ui/react-tabs/components-api/#tab-indicator', - title: 'TabIndicator', - }, - { pathname: '/base-ui/react-tabs/components-api/#tab-panel', title: 'TabPanel' }, - { - pathname: '/base-ui/react-table-pagination/components-api/#table-pagination', - title: 'TablePagination', - }, - { pathname: '/base-ui/react-tabs/components-api/#tabs-list', title: 'TabsList' }, - { pathname: '/base-ui/react-tabs/components-api/#tabs-root', title: 'TabsRoot' }, - { - pathname: '/base-ui/react-textarea-autosize/components-api/#textarea-autosize', - title: 'TextareaAutosize', - }, - { - pathname: '/base-ui/react-tooltip/components-api/#tooltip-arrow', - title: 'TooltipArrow', - }, - { - pathname: '/base-ui/react-tooltip/components-api/#tooltip-popup', - title: 'TooltipPopup', - }, - { - pathname: '/base-ui/react-tooltip/components-api/#tooltip-positioner', - title: 'TooltipPositioner', - }, - { - pathname: '/base-ui/react-tooltip/components-api/#tooltip-provider', - title: 'TooltipProvider', - }, - { - pathname: '/base-ui/react-tooltip/components-api/#tooltip-root', - title: 'TooltipRoot', - }, - { - pathname: '/base-ui/react-tooltip/components-api/#tooltip-trigger', - title: 'TooltipTrigger', - }, - { - pathname: '/base-ui/react-autocomplete/hooks-api/#use-autocomplete', - title: 'useAutocomplete', - }, - { - pathname: '/base-ui/react-checkbox/hooks-api/#use-checkbox-root', - title: 'useCheckboxRoot', - }, - { - pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-close', - title: 'useDialogClose', - }, - { - pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-popup', - title: 'useDialogPopup', - }, - { - pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-root', - title: 'useDialogRoot', - }, - { - pathname: '/base-ui/react-dialog/hooks-api/#use-dialog-trigger', - title: 'useDialogTrigger', - }, - { - pathname: '/base-ui/react-form-control/hooks-api/#use-form-control-context', - title: 'useFormControlContext', - }, - { - pathname: '/base-ui/react-number-field/hooks-api/#use-number-field-root', - title: 'useNumberFieldRoot', - }, - { - pathname: '/base-ui/react-progress/hooks-api/#use-progress-indicator', - title: 'useProgressIndicator', - }, - { - pathname: '/base-ui/react-progress/hooks-api/#use-progress-root', - title: 'useProgressRoot', - }, - { - pathname: '/base-ui/react-slider/hooks-api/#use-slider-control', - title: 'useSliderControl', - }, - { - pathname: '/base-ui/react-slider/hooks-api/#use-slider-indicator', - title: 'useSliderIndicator', - }, - { - pathname: '/base-ui/react-slider/hooks-api/#use-slider-output', - title: 'useSliderOutput', - }, - { - pathname: '/base-ui/react-slider/hooks-api/#use-slider-root', - title: 'useSliderRoot', - }, - { - pathname: '/base-ui/react-slider/hooks-api/#use-slider-thumb', - title: 'useSliderThumb', - }, - { - pathname: '/base-ui/react-snackbar/hooks-api/#use-snackbar', - title: 'useSnackbar', - }, - { - pathname: '/base-ui/react-switch/hooks-api/#use-switch-root', - title: 'useSwitchRoot', - }, - { pathname: '/base-ui/react-tabs/hooks-api/#use-tab', title: 'useTab' }, - { - pathname: '/base-ui/react-tabs/hooks-api/#use-tab-indicator', - title: 'useTabIndicator', - }, - { pathname: '/base-ui/react-tabs/hooks-api/#use-tab-panel', title: 'useTabPanel' }, - { pathname: '/base-ui/react-tabs/hooks-api/#use-tabs-list', title: 'useTabsList' }, - { pathname: '/base-ui/react-tabs/hooks-api/#use-tabs-root', title: 'useTabsRoot' }, -]; diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js similarity index 100% rename from docs/data/base/components/select/SelectIntroduction/system/index.js rename to docs/data/components/select/SelectIntroduction/system/index.js diff --git a/docs/data/base/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx similarity index 100% rename from docs/data/base/components/select/SelectIntroduction/system/index.tsx rename to docs/data/components/select/SelectIntroduction/system/index.tsx diff --git a/docs/data/base/components/select/select.md b/docs/data/components/select/select.md similarity index 100% rename from docs/data/base/components/select/select.md rename to docs/data/components/select/select.md diff --git a/docs/data/translations/api-docs/select-arrow/select-arrow.json b/docs/data/translations/api-docs/select-arrow/select-arrow.json new file mode 100644 index 000000000..3dfaaeafb --- /dev/null +++ b/docs/data/translations/api-docs/select-arrow/select-arrow.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "hideWhenUncentered": { + "description": "If true, the arrow is hidden when it can't point to the center of the anchor element." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-backdrop/select-backdrop.json b/docs/data/translations/api-docs/select-backdrop/select-backdrop.json new file mode 100644 index 000000000..17b35bc88 --- /dev/null +++ b/docs/data/translations/api-docs/select-backdrop/select-backdrop.json @@ -0,0 +1,14 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "container": { "description": "The container element to which the Backdrop is appended to." }, + "keepMounted": { + "description": "If true, the Backdrop remains mounted when the Select popup is closed." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-icon/select-icon.json b/docs/data/translations/api-docs/select-icon/select-icon.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/data/translations/api-docs/select-icon/select-icon.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-option-group-label/select-option-group-label.json b/docs/data/translations/api-docs/select-option-group-label/select-option-group-label.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/data/translations/api-docs/select-option-group-label/select-option-group-label.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-option-group/select-option-group.json b/docs/data/translations/api-docs/select-option-group/select-option-group.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/data/translations/api-docs/select-option-group/select-option-group.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-option-indicator/select-option-indicator.json b/docs/data/translations/api-docs/select-option-indicator/select-option-indicator.json new file mode 100644 index 000000000..9c4340d03 --- /dev/null +++ b/docs/data/translations/api-docs/select-option-indicator/select-option-indicator.json @@ -0,0 +1,13 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "keepMounted": { + "description": "If true, the item indicator remains mounted when the item is not selected." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-option/select-option.json b/docs/data/translations/api-docs/select-option/select-option.json new file mode 100644 index 000000000..b639a9b4a --- /dev/null +++ b/docs/data/translations/api-docs/select-option/select-option.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "An unstyled select item to be used within a Select.", + "propDescriptions": { + "disabled": { "description": "If true, the select option will be disabled." }, + "label": { + "description": "A text representation of the select option's content. Used for keyboard text navigation matching." + }, + "onClick": { "description": "The click handler for the select option." }, + "value": { "description": "The value of the select option." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-popup/select-popup.json b/docs/data/translations/api-docs/select-popup/select-popup.json new file mode 100644 index 000000000..4a1c0a206 --- /dev/null +++ b/docs/data/translations/api-docs/select-popup/select-popup.json @@ -0,0 +1,11 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "id": { "description": "The id of the popup element." }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-positioner/select-positioner.json b/docs/data/translations/api-docs/select-positioner/select-positioner.json new file mode 100644 index 000000000..410bc02b7 --- /dev/null +++ b/docs/data/translations/api-docs/select-positioner/select-positioner.json @@ -0,0 +1,43 @@ +{ + "componentDescription": "Renders the element that positions the Select popup.", + "propDescriptions": { + "alignment": { + "description": "The alignment of the Select element to the anchor element along its cross axis." + }, + "alignmentOffset": { + "description": "The offset of the Select element along its alignment axis." + }, + "anchor": { "description": "The anchor element to which the Select popup will be placed at." }, + "arrowPadding": { + "description": "Determines the padding between the arrow and the Select popup's edges. Useful when the popover popup has rounded corners via border-radius." + }, + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "collisionBoundary": { + "description": "The boundary that the Select element should be constrained to." + }, + "collisionPadding": { "description": "The padding of the collision boundary." }, + "container": { + "description": "The container element to which the Select popup will be appended to." + }, + "hideWhenDetached": { + "description": "If true, the Select will be hidden if it is detached from its anchor element due to differing clipping contexts." + }, + "keepMounted": { + "description": "Whether the select popup remains mounted in the DOM while closed." + }, + "positionStrategy": { + "description": "The CSS position strategy for positioning the Select popup element." + }, + "render": { "description": "A function to customize rendering of the component." }, + "side": { + "description": "The side of the anchor element that the Select element should align to." + }, + "sideOffset": { "description": "The gap between the anchor element and the Select element." }, + "sticky": { + "description": "If true, allow the Select to remain in stuck view while the anchor element is scrolled out of view." + } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-root/select-root.json b/docs/data/translations/api-docs/select-root/select-root.json new file mode 100644 index 000000000..df1f10fce --- /dev/null +++ b/docs/data/translations/api-docs/select-root/select-root.json @@ -0,0 +1,29 @@ +{ + "componentDescription": "", + "propDescriptions": { + "alignMethod": { + "description": "Determines if the select should align to the selected item inside the popup or the trigger element." + }, + "animated": { + "description": "If true, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." + }, + "defaultOpen": { "description": "If true, the Select is initially open." }, + "defaultValue": { "description": "The default value of the select." }, + "disabled": { "description": "If true, the Select is disabled." }, + "id": { "description": "The id of the Select." }, + "loop": { + "description": "If true, using keyboard navigation will wrap focus to the other end of the list once the end is reached." + }, + "name": { "description": "The name of the Select in the owning form." }, + "onOpenChange": { + "description": "Callback fired when the component requests to be opened or closed." + }, + "open": { + "description": "Allows to control whether the dropdown is open. This is a controlled counterpart of defaultOpen." + }, + "readOnly": { "description": "If true, the Select is read-only." }, + "required": { "description": "If true, the Select is required." }, + "value": { "description": "The value of the select." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json b/docs/data/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json new file mode 100644 index 000000000..47e423952 --- /dev/null +++ b/docs/data/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "keepMounted": { + "description": "Whether the component should be kept mounted when it is not rendered." + } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json b/docs/data/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json new file mode 100644 index 000000000..47e423952 --- /dev/null +++ b/docs/data/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "keepMounted": { + "description": "Whether the component should be kept mounted when it is not rendered." + } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-separator/select-separator.json b/docs/data/translations/api-docs/select-separator/select-separator.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/data/translations/api-docs/select-separator/select-separator.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-trigger/select-trigger.json b/docs/data/translations/api-docs/select-trigger/select-trigger.json new file mode 100644 index 000000000..814aa3e49 --- /dev/null +++ b/docs/data/translations/api-docs/select-trigger/select-trigger.json @@ -0,0 +1,15 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "disabled": { "description": "If true, the component is disabled." }, + "focusableWhenDisabled": { + "description": "If true, allows a disabled button to receive focus." + }, + "label": { "description": "Label of the button" }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-value/select-value.json b/docs/data/translations/api-docs/select-value/select-value.json new file mode 100644 index 000000000..491cf0963 --- /dev/null +++ b/docs/data/translations/api-docs/select-value/select-value.json @@ -0,0 +1,9 @@ +{ + "componentDescription": "", + "propDescriptions": { + "placeholder": { + "description": "The placeholder value to display when the value is empty (such as during SSR)." + } + }, + "classDescriptions": {} +} diff --git a/docs/pages/base-ui/api/use-select-backdrop.json b/docs/pages/base-ui/api/use-select-backdrop.json deleted file mode 100644 index 1df096cb5..000000000 --- a/docs/pages/base-ui/api/use-select-backdrop.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "parameters": {}, - "returnValue": {}, - "name": "useSelectBackdrop", - "filename": "/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts", - "imports": ["import { useSelectBackdrop } from '@base_ui/react/Select';"], - "demos": "
          " -} diff --git a/docs/pages/base-ui/api/use-select-option.json b/docs/pages/base-ui/api/use-select-option.json deleted file mode 100644 index b009baea3..000000000 --- a/docs/pages/base-ui/api/use-select-option.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "parameters": {}, - "returnValue": {}, - "name": "useSelectOption", - "filename": "/packages/mui-base/src/Select/Option/useSelectOption.ts", - "imports": ["import { useSelectOption } from '@base_ui/react/Select';"], - "demos": "
            " -} diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx index eccf5f9da..b30bcf151 100644 --- a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx @@ -8,7 +8,16 @@ import { useForkRef } from '../../utils/useForkRef'; import { mergeReactProps } from '../../utils/mergeReactProps'; import type { BaseUIComponentProps } from '../../utils/types'; import { commonStyleHooks } from '../utils/commonStyleHooks'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectArrow API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectArrow) + */ const SelectArrow = React.forwardRef(function SelectArrow( props: SelectArrow.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx index b93957c27..93296ff7d 100644 --- a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx @@ -12,7 +12,16 @@ import { commonStyleHooks } from '../utils/commonStyleHooks'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; const customStyleHookMapping: CustomStyleHookMapping = commonStyleHooks; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectBackdrop API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectBackdrop) + */ const SelectBackdrop = React.forwardRef(function SelectBackdrop( props: SelectBackdrop.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Icon/SelectIcon.tsx b/packages/mui-base/src/Select/Icon/SelectIcon.tsx index 09031defb..be52fb120 100644 --- a/packages/mui-base/src/Select/Icon/SelectIcon.tsx +++ b/packages/mui-base/src/Select/Icon/SelectIcon.tsx @@ -4,7 +4,16 @@ import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectIcon API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectIcon) + */ const SelectIcon = React.forwardRef(function SelectIcon( props: SelectIcon.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Option/SelectOption.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx index fab9884d7..c322facce 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -77,11 +77,11 @@ const InnerSelectOption = React.memo( * * Demos: * - * - [Select](https://mui.com/base-ui/react-select/) + * - [Select](https://base-ui.netlify.app/components/react-select/) * * API: * - * - [SelectOption API](https://mui.com/base-ui/react-select/components-api/#select-item) + * - [SelectOption API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOption) */ const SelectOption = React.forwardRef(function SelectOption( props: SelectOption.Props, diff --git a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx index c0ecfbdf1..e2cd3d36a 100644 --- a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx +++ b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx @@ -6,7 +6,16 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { SelectOptionGroupContext } from './SelectOptionGroupContext'; import { useSelectRootContext } from '../Root/SelectRootContext'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectOptionGroup API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOptionGroup) + */ const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( props: SelectOptionGroup.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx b/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx index b3a3d9190..1ce12afa1 100644 --- a/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx +++ b/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx @@ -8,7 +8,16 @@ import { useId } from '../../utils/useId'; import { useSelectOptionGroupContext } from '../OptionGroup/SelectOptionGroupContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useSelectRootContext } from '../Root/SelectRootContext'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectOptionGroupLabel API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOptionGroupLabel) + */ const SelectOptionGroupLabel = React.forwardRef(function SelectOptionGroupLabel( props: SelectOptionGroupLabel.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx index 803099625..48b470759 100644 --- a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx @@ -11,7 +11,16 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; const customStyleHookMapping: CustomStyleHookMapping = commonStyleHooks; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectOptionIndicator API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOptionIndicator) + */ const SelectOptionIndicator = React.forwardRef(function SelectOptionIndicator( props: SelectOptionIndicator.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.tsx index 53e718259..3bc3b304d 100644 --- a/packages/mui-base/src/Select/Popup/SelectPopup.tsx +++ b/packages/mui-base/src/Select/Popup/SelectPopup.tsx @@ -20,7 +20,16 @@ const customStyleHookMapping: CustomStyleHookMapping = { return value ? { 'data-select-exiting': '' } : null; }, }; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectPopup API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectPopup) + */ const SelectPopup = React.forwardRef(function SelectPopup( props: SelectPopup.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 0d03564f3..78ec27209 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -28,11 +28,11 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; * * Demos: * - * - [Menu](https://mui.com/base-ui/react-select/) + * - [Select](https://base-ui.netlify.app/components/react-select/) * * API: * - * - [SelectPositioner API](https://mui.com/base-ui/react-select/components-api/#select-positioner) + * - [SelectPositioner API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectPositioner) */ const SelectPositioner = React.forwardRef(function SelectPositioner( props: SelectPositioner.Props, diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index db35af1dd..47234e73d 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -3,7 +3,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { SelectRootContext } from './SelectRootContext'; import { useSelectRoot } from './useSelectRoot'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectRoot API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectRoot) + */ function SelectRoot(props: SelectRoot.Props) { const { animated = true, diff --git a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx index c55079d89..9eea9f69b 100644 --- a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx +++ b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.tsx @@ -3,7 +3,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { SelectScrollArrow } from '../ScrollArrow/SelectScrollArrow'; import type { BaseUIComponentProps } from '../../utils/types'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectScrollDownArrow API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectScrollDownArrow) + */ const SelectScrollDownArrow = React.forwardRef(function SelectScrollDownArrow( props: SelectScrollDownArrow.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx index 80d231771..6096261dd 100644 --- a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx +++ b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.tsx @@ -3,7 +3,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { SelectScrollArrow } from '../ScrollArrow/SelectScrollArrow'; import type { BaseUIComponentProps } from '../../utils/types'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectScrollUpArrow API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectScrollUpArrow) + */ const SelectScrollUpArrow = React.forwardRef(function SelectScrollUpArrow( props: SelectScrollUpArrow.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Separator/SelectSeparator.tsx b/packages/mui-base/src/Select/Separator/SelectSeparator.tsx index 06c1fee95..962961f19 100644 --- a/packages/mui-base/src/Select/Separator/SelectSeparator.tsx +++ b/packages/mui-base/src/Select/Separator/SelectSeparator.tsx @@ -3,7 +3,16 @@ import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectSeparator API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectSeparator) + */ const SelectSeparator = React.forwardRef(function SelectSeparator( props: SelectSeparator.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx index 8f51e1ca1..4df059a5f 100644 --- a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx +++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx @@ -8,7 +8,16 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { BaseUIComponentProps } from '../../utils/types'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectTrigger API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectTrigger) + */ const SelectTrigger = React.forwardRef(function SelectTrigger( props: SelectTrigger.Props, forwardedRef: React.ForwardedRef, diff --git a/packages/mui-base/src/Select/Value/SelectValue.tsx b/packages/mui-base/src/Select/Value/SelectValue.tsx index a90c4821c..5c0127a76 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.tsx @@ -1,7 +1,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useSelectRootContext } from '../Root/SelectRootContext'; - +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectValue API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectValue) + */ function SelectValue(props: SelectValue.Props) { const { children, placeholder } = props; const { label } = useSelectRootContext(); @@ -35,7 +44,7 @@ SelectValue.propTypes /* remove-proptypes */ = { PropTypes.func, PropTypes.number, PropTypes.shape({ - '__@iterator@68': PropTypes.func.isRequired, + '__@iterator@70': PropTypes.func.isRequired, }), PropTypes.shape({ children: PropTypes.node, From c51983220311f6fbd8f4c35936ecb50230702f34 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 10:58:15 +1000 Subject: [PATCH 41/94] Move to new docs --- .../select/{select.md => select.mdx} | 32 +++---------------- docs/next-env.d.ts | 1 + docs/src/styles/reset.css | 7 ++++ 3 files changed, 13 insertions(+), 27 deletions(-) rename docs/data/components/select/{select.md => select.mdx} (83%) diff --git a/docs/data/components/select/select.md b/docs/data/components/select/select.mdx similarity index 83% rename from docs/data/components/select/select.md rename to docs/data/components/select/select.mdx index 4c5111916..8bc215715 100644 --- a/docs/data/components/select/select.md +++ b/docs/data/components/select/select.mdx @@ -8,39 +8,17 @@ waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-sel # Select -

            Select provides users with a floating element containing a list of options to choose from.

            + -{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} + -{{"component": "modules/components/ComponentPageTabs.js"}} +## Introduction -{{"demo": "SelectIntroduction", "defaultCodeOpen": false, "bg": "gradient"}} + ## Installation -Base UI components are all available as a single package. - - - -```bash npm -npm install @base_ui/react -``` - -```bash yarn -yarn add @base_ui/react -``` - -```bash pnpm -pnpm add @base_ui/react -``` - - - -Once you have the package installed, import the component. - -```ts -import * as Select from '@base_ui/react/Select'; -``` + ## Anatomy diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index 4f11a03dc..fd36f9494 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/docs/src/styles/reset.css b/docs/src/styles/reset.css index 7b809609a..f2899ca4a 100644 --- a/docs/src/styles/reset.css +++ b/docs/src/styles/reset.css @@ -2,6 +2,13 @@ body { margin: 0; /* background-color: var(--gray-surface-2); */ padding-top: 49px; + font-family: 'Inter', sans-serif; +} + +*, +*::before, +*::after { + box-sizing: border-box; } ::selection { From 7082bb17f82e26a57a9841b2ea38dbdb41b15bea Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 11:58:04 +1000 Subject: [PATCH 42/94] Add OptionText subcomponent --- docs/data/api/select-option-text.json | 19 ++++ docs/data/api/select-positioner.json | 4 +- docs/data/api/select-value.json | 6 +- .../select/SelectIntroduction/system/index.js | 6 +- .../SelectIntroduction/system/index.tsx | 6 +- docs/data/components/select/select.mdx | 4 +- .../select-option-text.json | 10 ++ .../api-docs/select-value/select-value.json | 6 +- .../OptionText/SelectOptionText.test.tsx | 20 ++++ .../Select/OptionText/SelectOptionText.tsx | 95 +++++++++++++++++++ .../src/Select/Popup/useSelectPopup.ts | 4 +- .../Select/Positioner/SelectPositioner.tsx | 18 +++- .../Positioner/SelectPositionerContext.ts | 2 + .../Select/Positioner/useSelectPositioner.tsx | 4 +- .../src/Select/Root/useSelectRoot.tsx | 3 + .../Select/ScrollArrow/SelectScrollArrow.tsx | 14 ++- .../mui-base/src/Select/Value/SelectValue.tsx | 48 ++++++++-- packages/mui-base/src/Select/index.barrel.ts | 1 + packages/mui-base/src/Select/index.ts | 1 + 19 files changed, 237 insertions(+), 34 deletions(-) create mode 100644 docs/data/api/select-option-text.json create mode 100644 docs/data/translations/api-docs/select-option-text/select-option-text.json create mode 100644 packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx create mode 100644 packages/mui-base/src/Select/OptionText/SelectOptionText.tsx diff --git a/docs/data/api/select-option-text.json b/docs/data/api/select-option-text.json new file mode 100644 index 000000000..ec5d5b486 --- /dev/null +++ b/docs/data/api/select-option-text.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
            | string" } }, + "render": { "type": { "name": "union", "description": "element
            | func" } } + }, + "name": "SelectOptionText", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectOptionText = Select.OptionText;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectOptionText", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/data/api/select-positioner.json b/docs/data/api/select-positioner.json index 36d0d520d..b8b2f3bb7 100644 --- a/docs/data/api/select-positioner.json +++ b/docs/data/api/select-positioner.json @@ -5,7 +5,7 @@ "name": "enum", "description": "'center'
            | 'end'
            | 'start'" }, - "default": "'center'" + "default": "'start'" }, "alignmentOffset": { "type": { "name": "number" }, "default": "0" }, "anchor": { @@ -35,7 +35,7 @@ "keepMounted": { "type": { "name": "bool" }, "default": "false" }, "positionStrategy": { "type": { "name": "enum", "description": "'absolute'
            | 'fixed'" }, - "default": "'fixed'" + "default": "'absolute'" }, "render": { "type": { "name": "union", "description": "element
            | func" } }, "side": { diff --git a/docs/data/api/select-value.json b/docs/data/api/select-value.json index 72d826363..f91f2d6ba 100644 --- a/docs/data/api/select-value.json +++ b/docs/data/api/select-value.json @@ -1,5 +1,9 @@ { - "props": { "placeholder": { "type": { "name": "string" } } }, + "props": { + "className": { "type": { "name": "union", "description": "func
            | string" } }, + "placeholder": { "type": { "name": "string" } }, + "render": { "type": { "name": "union", "description": "element
            | func" } } + }, "name": "SelectValue", "imports": [ "import * as Select from '@base_ui/react/Select';\nconst SelectValue = Select.Value;" diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index e5bdf0acc..217a28495 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -71,7 +71,7 @@ export default function UnstyledSelectIntroduction() { - +
            @@ -80,7 +80,7 @@ export default function UnstyledSelectIntroduction() { } /> - Select food... + Select food... {entries.map(([group, items]) => ( @@ -94,7 +94,7 @@ export default function UnstyledSelectIntroduction() { disabled={item.value === 'banana'} > } /> - {item.label} + {item.label} ))} diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index e5bdf0acc..217a28495 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -71,7 +71,7 @@ export default function UnstyledSelectIntroduction() { - +
            @@ -80,7 +80,7 @@ export default function UnstyledSelectIntroduction() { } /> - Select food... + Select food... {entries.map(([group, items]) => ( @@ -94,7 +94,7 @@ export default function UnstyledSelectIntroduction() { disabled={item.value === 'banana'} > } /> - {item.label} + {item.label} ))} diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index 8bc215715..c8cae915d 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -1,7 +1,7 @@ --- productId: base-ui title: React Select components and hook -components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow +components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionText, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- @@ -32,6 +32,7 @@ Selects are implemented using a collection of related components: - `` renders the select popup's positioning element. - `` renders the select popup itself. - `` renders an option, placed inside the popup. +- `` renders the text of an option. - `` renders an option indicator inside an option to indicate it's selected (e.g. a check icon). - `` renders an option group, wrapping `` components. - `` renders a label for an option group. @@ -56,6 +57,7 @@ Selects are implemented using a collection of related components: + diff --git a/docs/data/translations/api-docs/select-option-text/select-option-text.json b/docs/data/translations/api-docs/select-option-text/select-option-text.json new file mode 100644 index 000000000..4bc12cf1e --- /dev/null +++ b/docs/data/translations/api-docs/select-option-text/select-option-text.json @@ -0,0 +1,10 @@ +{ + "componentDescription": "", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, + "classDescriptions": {} +} diff --git a/docs/data/translations/api-docs/select-value/select-value.json b/docs/data/translations/api-docs/select-value/select-value.json index 491cf0963..900c8e048 100644 --- a/docs/data/translations/api-docs/select-value/select-value.json +++ b/docs/data/translations/api-docs/select-value/select-value.json @@ -1,9 +1,13 @@ { "componentDescription": "", "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, "placeholder": { "description": "The placeholder value to display when the value is empty (such as during SSR)." - } + }, + "render": { "description": "A function to customize rendering of the component." } }, "classDescriptions": {} } diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx new file mode 100644 index 000000000..87f30a38e --- /dev/null +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render( + + + {node} + + , + ); + }, + })); +}); diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx new file mode 100644 index 000000000..b8a3443ae --- /dev/null +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useForkRef } from '../../utils/useForkRef'; +import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; +import { useSelectOptionContext } from '../Option/SelectOptionContext'; +/** + * + * Demos: + * + * - [Select](https://base-ui.netlify.app/components/react-select/) + * + * API: + * + * - [SelectOptionText API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOptionText) + */ +const SelectOptionText = React.forwardRef(function SelectOptionText( + props: SelectOptionText.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, ...otherProps } = props; + + const { open, triggerElement, valueRef, popupRef } = useSelectRootContext(); + const { isPositioned, setOptionTextOffset } = useSelectPositionerContext(); + const { selected } = useSelectOptionContext(); + + const textRef = React.useRef(null); + + const mergedRef = useForkRef(forwardedRef, textRef); + + const ownerState: SelectOptionText.OwnerState = React.useMemo(() => ({}), []); + + useEnhancedEffect(() => { + if ( + !open || + !isPositioned || + !selected || + !triggerElement || + !valueRef.current || + !textRef.current || + !popupRef.current + ) { + return; + } + + const triggerX = triggerElement.getBoundingClientRect().left; + const valueX = valueRef.current.getBoundingClientRect().left; + const popupX = popupRef.current.getBoundingClientRect().left; + const textX = textRef.current.getBoundingClientRect().left; + + const triggerDiff = valueX - triggerX; + const popupDiff = textX - popupX; + + setOptionTextOffset(triggerDiff - popupDiff); + }, [open, isPositioned, popupRef, selected, setOptionTextOffset, triggerElement, valueRef]); + + const { renderElement } = useComponentRenderer({ + ref: mergedRef, + render: render ?? 'div', + className, + ownerState, + extraProps: otherProps, + }); + + return renderElement(); +}); + +SelectOptionText.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), +} as any; + +export { SelectOptionText }; + +namespace SelectOptionText { + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index a4a9bcec9..2197a0b79 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -14,7 +14,6 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { getPopupProps: getRootPopupProps, alignMethod, selectedIndex, - innerFallback, touchModality, } = useSelectRootContext(); @@ -29,14 +28,13 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { overflowY: 'auto', ...(alignMethod && hasSelectedIndex && - !innerFallback && !touchModality && { scrollbarWidth: 'none', }), }, }); }, - [getRootPopupProps, alignMethod, hasSelectedIndex, innerFallback, touchModality], + [getRootPopupProps, alignMethod, hasSelectedIndex, touchModality], ); return React.useMemo( diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 78ec27209..88aa43838 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -40,12 +40,12 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( ) { const { anchor, - positionStrategy = 'fixed', + positionStrategy = 'absolute', className, render, keepMounted = false, side = 'bottom', - alignment = 'center', + alignment = 'start', sideOffset = 0, alignmentOffset = 0, collisionBoundary, @@ -94,8 +94,13 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( }; }, [id, setControlId]); + const [optionTextOffset, setOptionTextOffset] = React.useState(null); const [selectedIndexOnMount, setSelectedIndexOnMount] = React.useState(selectedIndex); + if (!mounted && optionTextOffset !== null) { + setOptionTextOffset(null); + } + const selectedIndexRef = useLatestRef(selectedIndex); useEnhancedEffect(() => { @@ -120,7 +125,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( side, sideOffset, alignment, - alignmentOffset, + alignmentOffset: optionTextOffset ?? alignmentOffset, arrowPadding, collisionBoundary, collisionPadding, @@ -170,6 +175,8 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( arrowUncentered: positioner.arrowUncentered, arrowStyles: positioner.arrowStyles, floatingContext: positioner.floatingContext, + optionTextOffset, + setOptionTextOffset, }), [ positioner.isPositioned, @@ -179,6 +186,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( positioner.arrowUncentered, positioner.arrowStyles, positioner.floatingContext, + optionTextOffset, ], ); @@ -290,7 +298,7 @@ SelectPositioner.propTypes /* remove-proptypes */ = { // └─────────────────────────────────────────────────────────────────────┘ /** * The alignment of the Select element to the anchor element along its cross axis. - * @default 'center' + * @default 'start' */ alignment: PropTypes.oneOf(['center', 'end', 'start']), /** @@ -368,7 +376,7 @@ SelectPositioner.propTypes /* remove-proptypes */ = { keepMounted: PropTypes.bool, /** * The CSS position strategy for positioning the Select popup element. - * @default 'fixed' + * @default 'absolute' */ positionStrategy: PropTypes.oneOf(['absolute', 'fixed']), /** diff --git a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts index eab042161..255636c13 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts +++ b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts @@ -15,6 +15,8 @@ export interface SelectPositionerContext { arrowUncentered: boolean; arrowStyles: React.CSSProperties; isPositioned: boolean; + optionTextOffset: number | null; + setOptionTextOffset: React.Dispatch>; } export const SelectPositionerContext = React.createContext(null); diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 5427c0c73..698394b3d 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -115,7 +115,7 @@ export namespace useSelectPositioner { | (() => Element | VirtualElement | null); /** * The CSS position strategy for positioning the Select popup element. - * @default 'fixed' + * @default 'absolute' */ positionStrategy?: 'absolute' | 'fixed'; /** @@ -134,7 +134,7 @@ export namespace useSelectPositioner { sideOffset?: number; /** * The alignment of the Select element to the anchor element along its cross axis. - * @default 'center' + * @default 'start' */ alignment?: 'start' | 'end' | 'center'; /** diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 27ca15cb9..bd65b5906 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -63,6 +63,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const valuesRef = React.useRef>([]); const selectionRef = React.useRef({ allowMouseUp: false, allowSelect: false }); const overflowRef = React.useRef({ top: 0, bottom: 0, left: 0, right: 0 }); + const valueRef = React.useRef(null); const [open, setOpenUnwrapped] = useControlled({ controlled: openParam, @@ -246,6 +247,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setInnerFallback, touchModality, setTouchModality, + valueRef, }), [ fieldControlValidation, @@ -358,5 +360,6 @@ export namespace useSelectRoot { setInnerFallback: React.Dispatch>; touchModality: boolean; setTouchModality: React.Dispatch>; + valueRef: React.RefObject; } } diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index 4ce820c07..952c71b07 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -7,6 +7,7 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; import { useEventCallback } from '../../utils/useEventCallback'; +import { ownerWindow } from '../../utils/owner'; /** * @ignore - internal component. @@ -44,6 +45,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( (externalProps = {}) => mergeReactProps<'div'>(externalProps, { 'aria-hidden': true, + children: '▼', style: { position: 'absolute', zIndex: 2147483647, // max z-index @@ -64,9 +66,11 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( const msElapsed = currentNow - prevNow; prevNow = currentNow; - const pixelsToScroll = Math.min( - msElapsed / 2, - popupRef.current.scrollHeight - popupRef.current.clientHeight, + const pixelsToScroll = Math.floor( + Math.min( + msElapsed / 2, + popupRef.current.scrollHeight - popupRef.current.clientHeight, + ) || 1, ); const isScrolledToTop = popupRef.current.scrollTop === 0; @@ -132,12 +136,16 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( return undefined; } + const win = ownerWindow(popupElement); + popupElement.addEventListener('wheel', handleScrollArrowRendered); popupElement.addEventListener('scroll', handleScrollArrowRendered); + win.addEventListener('resize', handleScrollArrowRendered); return () => { popupElement.removeEventListener('wheel', handleScrollArrowRendered); popupElement.removeEventListener('scroll', handleScrollArrowRendered); + win.removeEventListener('resize', handleScrollArrowRendered); }; }, [inert, popupRef, direction, handleScrollArrowRendered]); diff --git a/packages/mui-base/src/Select/Value/SelectValue.tsx b/packages/mui-base/src/Select/Value/SelectValue.tsx index 5c0127a76..6d99e68c7 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.tsx @@ -1,6 +1,10 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import type { BaseUIComponentProps } from '../../utils/types'; +import { useForkRef } from '../../utils/useForkRef'; + /** * * Demos: @@ -11,18 +15,34 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; * * - [SelectValue API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectValue) */ -function SelectValue(props: SelectValue.Props) { - const { children, placeholder } = props; - const { label } = useSelectRootContext(); - return ( - - {typeof children === 'function' ? children(label) : label || placeholder} - - ); -} +const SelectValue = React.forwardRef(function SelectValue( + props: SelectValue.Props, + forwardedRef: React.ForwardedRef, +) { + const { className, render, children, placeholder, ...otherProps } = props; + const { label, valueRef } = useSelectRootContext(); + + const mergedRef = useForkRef(forwardedRef, valueRef); + + const ownerState: SelectValue.OwnerState = React.useMemo(() => ({}), []); + + const { renderElement } = useComponentRenderer({ + propGetter: () => ({ + children: typeof children === 'function' ? children(label) : label || placeholder, + }), + render: render ?? 'span', + className, + ownerState, + ref: mergedRef, + extraProps: otherProps, + }); + + return renderElement(); +}); namespace SelectValue { - export interface Props { + export interface OwnerState {} + export interface Props extends Omit, 'children'> { children?: React.ReactNode | ((value: string) => React.ReactNode); /** * The placeholder value to display when the value is empty (such as during SSR). @@ -55,10 +75,18 @@ SelectValue.propTypes /* remove-proptypes */ = { PropTypes.string, PropTypes.bool, ]), + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** * The placeholder value to display when the value is empty (such as during SSR). */ placeholder: PropTypes.string, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; export { SelectValue }; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index 96326d6bb..65633b150 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -13,3 +13,4 @@ export { SelectScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; export { SelectSeparator } from './Separator/SelectSeparator'; export { SelectIcon } from './Icon/SelectIcon'; export { SelectArrow } from './Arrow/SelectArrow'; +export { SelectOptionText } from './OptionText/SelectOptionText'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 2edc671ec..ef3075a5e 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -13,3 +13,4 @@ export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/Sele export { SelectSeparator as Separator } from './Separator/SelectSeparator'; export { SelectIcon as Icon } from './Icon/SelectIcon'; export { SelectArrow as Arrow } from './Arrow/SelectArrow'; +export { SelectOptionText as OptionText } from './OptionText/SelectOptionText'; From 3ccac6a7e7892275af7920d9d1c97a7b7c41b4b9 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 12:14:15 +1000 Subject: [PATCH 43/94] Avoid optionTextOffset for inert positioning --- docs/data/pages.ts | 1 + .../src/Select/OptionText/SelectOptionText.tsx | 18 ++++++++++++++++-- .../src/Select/Positioner/SelectPositioner.tsx | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/data/pages.ts b/docs/data/pages.ts index e26c851b4..224670385 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -37,6 +37,7 @@ const pages: readonly RouteMetadata[] = [ { pathname: '/components/react-progress', title: 'Progress' }, { pathname: '/components/react-radio-group', title: 'Radio Group' }, { pathname: '/components/react-separator', title: 'Separator' }, + { pathname: '/components/react-select', title: 'Select' }, { pathname: '/components/react-slider', title: 'Slider' }, { pathname: '/components/react-switch', title: 'Switch' }, { pathname: '/components/react-tabs', title: 'Tabs' }, diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index b8a3443ae..f394501c2 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -7,6 +7,7 @@ import { useForkRef } from '../../utils/useForkRef'; import { useSelectRootContext } from '../Root/SelectRootContext'; import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; import { useSelectOptionContext } from '../Option/SelectOptionContext'; + /** * * Demos: @@ -23,10 +24,13 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( ) { const { className, render, ...otherProps } = props; - const { open, triggerElement, valueRef, popupRef } = useSelectRootContext(); + const { open, triggerElement, valueRef, popupRef, touchModality, innerFallback } = + useSelectRootContext(); const { isPositioned, setOptionTextOffset } = useSelectPositionerContext(); const { selected } = useSelectOptionContext(); + const inert = touchModality || innerFallback; + const textRef = React.useRef(null); const mergedRef = useForkRef(forwardedRef, textRef); @@ -35,6 +39,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( useEnhancedEffect(() => { if ( + inert || !open || !isPositioned || !selected || @@ -55,7 +60,16 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( const popupDiff = textX - popupX; setOptionTextOffset(triggerDiff - popupDiff); - }, [open, isPositioned, popupRef, selected, setOptionTextOffset, triggerElement, valueRef]); + }, [ + inert, + open, + isPositioned, + popupRef, + selected, + setOptionTextOffset, + triggerElement, + valueRef, + ]); const { renderElement } = useComponentRenderer({ ref: mergedRef, diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 88aa43838..a5ca48a00 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -97,7 +97,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const [optionTextOffset, setOptionTextOffset] = React.useState(null); const [selectedIndexOnMount, setSelectedIndexOnMount] = React.useState(selectedIndex); - if (!mounted && optionTextOffset !== null) { + if (optionTextOffset !== null && (!mounted || innerFallback || touchModality)) { setOptionTextOffset(null); } From 5d6fc6fee2809ac2937a5c2bfa7614d6f9b4b239 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 14:44:14 +1000 Subject: [PATCH 44/94] Tests --- docs/data/api/select-root.json | 1 + docs/data/api/select-value.json | 3 + .../SelectIntroduction/system/index.tsx | 2 +- .../api-docs/select-root/select-root.json | 3 + .../src/Select/Option/SelectOption.test.tsx | 242 ++++++++++++++++++ .../OptionGroup/SelectOptionGroup.test.tsx | 37 +++ .../Select/OptionGroup/SelectOptionGroup.tsx | 15 +- .../src/Select/Root/SelectRoot.test.tsx | 242 ++++++++++++++++++ .../mui-base/src/Select/Root/SelectRoot.tsx | 12 +- .../src/Select/Root/useSelectRoot.tsx | 2 +- .../src/Select/Value/SelectValue.test.tsx | 46 ++++ .../mui-base/src/Select/Value/SelectValue.tsx | 14 +- 12 files changed, 606 insertions(+), 13 deletions(-) create mode 100644 packages/mui-base/src/Select/Value/SelectValue.test.tsx diff --git a/docs/data/api/select-root.json b/docs/data/api/select-root.json index c6b67da92..7db5208bb 100644 --- a/docs/data/api/select-root.json +++ b/docs/data/api/select-root.json @@ -12,6 +12,7 @@ "loop": { "type": { "name": "bool" }, "default": "true" }, "name": { "type": { "name": "string" } }, "onOpenChange": { "type": { "name": "func" } }, + "onValueChange": { "type": { "name": "func" } }, "open": { "type": { "name": "bool" } }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, "required": { "type": { "name": "bool" }, "default": "false" }, diff --git a/docs/data/api/select-value.json b/docs/data/api/select-value.json index f91f2d6ba..174fb3be2 100644 --- a/docs/data/api/select-value.json +++ b/docs/data/api/select-value.json @@ -9,7 +9,10 @@ "import * as Select from '@base_ui/react/Select';\nconst SelectValue = Select.Value;" ], "classes": [], + "spread": true, + "themeDefaultProps": true, "muiName": "SelectValue", + "forwardsRefTo": "HTMLSpanElement", "filename": "/packages/mui-base/src/Select/Value/SelectValue.tsx", "inheritance": null, "demos": "", diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 217a28495..25ceab09e 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -151,7 +151,7 @@ const SelectPopup = styled(Select.Popup)` 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 16px); + min-width: calc(var(--anchor-width) + 20px); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/translations/api-docs/select-root/select-root.json b/docs/data/translations/api-docs/select-root/select-root.json index df1f10fce..9e50b677f 100644 --- a/docs/data/translations/api-docs/select-root/select-root.json +++ b/docs/data/translations/api-docs/select-root/select-root.json @@ -18,6 +18,9 @@ "onOpenChange": { "description": "Callback fired when the component requests to be opened or closed." }, + "onValueChange": { + "description": "Callback fired when the value of the select changes. Use when controlled." + }, "open": { "description": "Allows to control whether the dropdown is open. This is a controlled counterpart of defaultOpen." }, diff --git a/packages/mui-base/src/Select/Option/SelectOption.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx index e6194bb4a..63b342e5b 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.test.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.test.tsx @@ -1,6 +1,11 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; +import { fireEvent, flushMicrotasks, screen } from '@mui/internal-test-utils'; import { createRenderer, describeConformance } from '#test-utils'; +import { expect } from 'chai'; +import userEvent from '@testing-library/user-event'; + +const user = userEvent.setup(); describe('', () => { const { render } = createRenderer(); @@ -15,4 +20,241 @@ describe('', () => { ); }, })); + + it('should select the option and close popup when clicked', async () => { + await render( + + + + + + one + + , + ); + + const value = screen.getByTestId('value'); + const trigger = screen.getByTestId('trigger'); + const positioner = screen.getByTestId('positioner'); + + expect(value.textContent).to.equal('null'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + fireEvent.click(screen.getByText('one')); + + expect(value.textContent).to.equal('one'); + + expect(positioner).not.toBeVisible(); + }); + + it('navigating with keyboard should highlight option', async () => { + await render( + + + + + + + one + two + three + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByText('one')).toHaveFocus(); + + await user.keyboard('{ArrowDown}'); + + expect(screen.getByText('two')).toHaveFocus(); + }); + + it('should select option when Enter key is pressed', async () => { + await render( + + + + + + + one + two + + + , + ); + + const trigger = screen.getByTestId('trigger'); + const value = screen.getByTestId('value'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + await user.keyboard('{ArrowDown}'); + await user.keyboard('{Enter}'); + + expect(value.textContent).to.equal('two'); + }); + + it('should not select disabled option', async () => { + await render( + + + + + + + one + + two + + + + , + ); + + const trigger = screen.getByTestId('trigger'); + const value = screen.getByTestId('value'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + fireEvent.click(screen.getByText('two')); + + expect(value.textContent).to.equal(''); + }); + + it('should focus the selected option upon opening the popup', async () => { + await render( + + + + + + + one + two + three + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'one' })).toHaveFocus(); + + await userEvent.keyboard('{ArrowDown}'); + + expect(screen.getByRole('option', { name: 'two' })).toHaveFocus(); + + await userEvent.keyboard('{ArrowUp}'); + + expect(screen.getByRole('option', { name: 'one' })).toHaveFocus(); + + await userEvent.keyboard('{ArrowUp}'); + + expect(screen.getByRole('option', { name: 'three' })).toHaveFocus(); + + fireEvent.click(screen.getByRole('option', { name: 'three' })); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'three', hidden: false })).toHaveFocus(); + }); + + describe('style hooks', () => { + it('should apply data-highlighted attribute when option is highlighted', async () => { + await render( + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'a' })).to.have.attribute( + 'data-highlighted', + 'true', + ); + expect(screen.getByRole('option', { name: 'b' })).not.to.have.attribute('data-highlighted'); + + await user.keyboard('{ArrowDown}'); + + expect(screen.getByRole('option', { name: 'b' })).to.have.attribute( + 'data-highlighted', + 'true', + ); + expect(screen.getByRole('option', { name: 'a' })).not.to.have.attribute('data-highlighted'); + }); + + it('should apply data-selected attribute when option is selected', async () => { + await render( + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'a', hidden: false })).not.to.have.attribute( + 'data-selected', + ); + expect(screen.getByRole('option', { name: 'b', hidden: false })).not.to.have.attribute( + 'data-selected', + ); + + fireEvent.click(screen.getByRole('option', { name: 'a', hidden: false })); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'a', hidden: false })).to.have.attribute( + 'data-selected', + 'true', + ); + expect(screen.getByRole('option', { name: 'b', hidden: false })).not.to.have.attribute( + 'data-selected', + ); + }); + }); }); diff --git a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx index 356221dd7..34df3009b 100644 --- a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx +++ b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; +import { screen } from '@mui/internal-test-utils'; +import { expect } from 'chai'; describe('', () => { const { render } = createRenderer(); @@ -15,4 +17,39 @@ describe('', () => { ); }, })); + + it('should render option group with label', async () => { + await render( + + + + Fruits + Apple + Banana + + + , + ); + + expect(screen.getByRole('group')).to.have.attribute('aria-labelledby'); + expect(screen.getByText('Fruits')).toBeVisible(); + }); + + it('should associate label with option group', async () => { + await render( + + + + Vegetables + Carrot + Lettuce + + + , + ); + + const optionGroup = screen.getByRole('group'); + const label = screen.getByText('Vegetables'); + expect(optionGroup).to.have.attribute('aria-labelledby', label.id); + }); }); diff --git a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx index e2cd3d36a..d59d0b701 100644 --- a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx +++ b/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx @@ -6,6 +6,7 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { SelectOptionGroupContext } from './SelectOptionGroupContext'; import { useSelectRootContext } from '../Root/SelectRootContext'; + /** * * Demos: @@ -66,6 +67,13 @@ const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( ); }); +namespace SelectOptionGroup { + export interface OwnerState { + open: boolean; + } + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} + SelectOptionGroup.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -85,11 +93,4 @@ SelectOptionGroup.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -namespace SelectOptionGroup { - export interface OwnerState { - open: boolean; - } - export interface Props extends BaseUIComponentProps<'div', OwnerState> {} -} - export { SelectOptionGroup }; diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index e69de29bb..649b83b29 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -0,0 +1,242 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { fireEvent, flushMicrotasks, screen } from '@mui/internal-test-utils'; +import { createRenderer } from '#test-utils'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import userEvent from '@testing-library/user-event'; + +const user = userEvent.setup(); + +describe('', () => { + const { render } = createRenderer(); + + describe('prop: defaultValue', () => { + it('should select the option by default', async () => { + await render( + + + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'b', hidden: false })).to.have.attribute( + 'data-selected', + 'true', + ); + }); + }); + + describe('prop: value', () => { + it('should select the option specified by the value prop', async () => { + await render( + + + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'b', hidden: false })).to.have.attribute( + 'data-selected', + 'true', + ); + }); + + it('should update the selected option when the value prop changes', async () => { + const { rerender } = await render( + + + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'a', hidden: false })).to.have.attribute( + 'data-selected', + 'true', + ); + + rerender( + + + + + + + a + b + + + , + ); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'b', hidden: false })).to.have.attribute( + 'data-selected', + 'true', + ); + }); + }); + + describe('prop: onValueChange', () => { + it('should call onValueChange when an option is selected', async () => { + const handleValueChange = spy(); + + function App() { + const [value, setValue] = React.useState(''); + + return ( + { + setValue(newValue); + handleValueChange(newValue); + }} + animated={false} + > + + + + + + a + b + + + + ); + } + + await render(); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + await user.click(screen.getByRole('option', { name: 'b', hidden: false })); + + expect(handleValueChange.args[0][0]).to.equal('b'); + }); + }); + + describe('prop: defaultOpen', () => { + it('should open the select by default', async () => { + await render( + + + + + + + a + b + + + , + ); + + expect(screen.getByRole('listbox', { hidden: false })).toBeVisible(); + }); + }); + + describe('prop: open', () => { + it('should control the open state of the select', async () => { + function ControlledSelect({ open }: { open: boolean }) { + return ( + + + + + + + a + b + + + + ); + } + + const { rerender } = await render(); + + expect(screen.queryByRole('listbox')).not.to.equal(null); + + rerender(); + + await flushMicrotasks(); + + expect(screen.getByRole('listbox', { hidden: false })).toBeVisible(); + }); + }); + + describe('prop: onOpenChange', () => { + it('should call onOpenChange when the select is opened or closed', async () => { + const handleOpenChange = spy(); + + await render( + + + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + await user.click(trigger); + expect(handleOpenChange.callCount).to.equal(1); + expect(handleOpenChange.args[0][0]).to.equal(true); + + await user.click(trigger); + expect(handleOpenChange.callCount).to.equal(2); + expect(handleOpenChange.args[1][0]).to.equal(false); + }); + }); +}); diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index 47234e73d..dff91f4a7 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -20,6 +20,7 @@ function SelectRoot(props: SelectRoot.Props) { name, children, value, + onValueChange, defaultValue, defaultOpen = false, disabled = false, @@ -39,8 +40,9 @@ function SelectRoot(props: SelectRoot.Props) { defaultOpen, open, alignMethod, - value, defaultValue, + value, + onValueChange, }); const context: SelectRootContext = React.useMemo( @@ -97,6 +99,10 @@ namespace SelectRoot { * The value of the select. */ value?: string; + /** + * Callback fired when the value of the select changes. Use when controlled. + */ + onValueChange?: (value: string, event?: Event) => void; /** * The default value of the select. */ @@ -185,6 +191,10 @@ SelectRoot.propTypes /* remove-proptypes */ = { * Callback fired when the component requests to be opened or closed. */ onOpenChange: PropTypes.func, + /** + * Callback fired when the value of the select changes. Use when controlled. + */ + onValueChange: PropTypes.func, /** * Allows to control whether the dropdown is open. * This is a controlled counterpart of `defaultOpen`. diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index bd65b5906..78d483349 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -7,10 +7,10 @@ import { useFloatingRootContext, useInnerOffset, useInteractions, - UseInteractionsReturn, useListNavigation, useRole, useTypeahead, + type UseInteractionsReturn, type FloatingRootContext, type SideObject, } from '@floating-ui/react'; diff --git a/packages/mui-base/src/Select/Value/SelectValue.test.tsx b/packages/mui-base/src/Select/Value/SelectValue.test.tsx new file mode 100644 index 000000000..6bd665549 --- /dev/null +++ b/packages/mui-base/src/Select/Value/SelectValue.test.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { screen } from '@mui/internal-test-utils'; +import { createRenderer, describeConformance } from '#test-utils'; +import { expect } from 'chai'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLSpanElement, + render(node) { + return render( + + {node} + , + ); + }, + })); + + describe('prop: placeholder', () => { + it('should render the placeholder when value is empty', async () => { + await render( + + + , + ); + expect(screen.getByText('test')).not.to.equal(null); + }); + + it('should render the value when value is not empty', async () => { + await render( + + + + + two + + , + ); + + const value = screen.getByTestId('value'); + expect(value.textContent).to.equal('two'); + }); + }); +}); diff --git a/packages/mui-base/src/Select/Value/SelectValue.tsx b/packages/mui-base/src/Select/Value/SelectValue.tsx index 6d99e68c7..8be0e103c 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.tsx @@ -4,6 +4,7 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { BaseUIComponentProps } from '../../utils/types'; import { useForkRef } from '../../utils/useForkRef'; +import { mergeReactProps } from '../../utils/mergeReactProps'; /** * @@ -20,16 +21,23 @@ const SelectValue = React.forwardRef(function SelectValue( forwardedRef: React.ForwardedRef, ) { const { className, render, children, placeholder, ...otherProps } = props; + const { label, valueRef } = useSelectRootContext(); const mergedRef = useForkRef(forwardedRef, valueRef); const ownerState: SelectValue.OwnerState = React.useMemo(() => ({}), []); + const getValueProps = React.useCallback( + (externalProps = {}) => + mergeReactProps(externalProps, { + children: typeof children === 'function' ? children(label) : label || placeholder, + }), + [children, label, placeholder], + ); + const { renderElement } = useComponentRenderer({ - propGetter: () => ({ - children: typeof children === 'function' ? children(label) : label || placeholder, - }), + propGetter: getValueProps, render: render ?? 'span', className, ownerState, From fb91223a49fc4ac078b40a000a0af2f4ad5e5eae Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 17:04:29 +1000 Subject: [PATCH 45/94] Remove pages/ --- .../base-ui/react-select/[docsTab]/index.js | 186 ------------------ 1 file changed, 186 deletions(-) delete mode 100644 docs/pages/base-ui/react-select/[docsTab]/index.js diff --git a/docs/pages/base-ui/react-select/[docsTab]/index.js b/docs/pages/base-ui/react-select/[docsTab]/index.js deleted file mode 100644 index 384697de2..000000000 --- a/docs/pages/base-ui/react-select/[docsTab]/index.js +++ /dev/null @@ -1,186 +0,0 @@ -import * as React from 'react'; -import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2'; -import AppFrame from 'docs/src/modules/components/AppFrame'; -import * as pageProps from 'docs-base/data/base/components/select/select.md?@mui/markdown'; -import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; -import SelectArrowApiJsonPageContent from '../../api/select-arrow.json'; -import SelectBackdropApiJsonPageContent from '../../api/select-backdrop.json'; -import SelectIconApiJsonPageContent from '../../api/select-icon.json'; -import SelectOptionApiJsonPageContent from '../../api/select-option.json'; -import SelectOptionGroupApiJsonPageContent from '../../api/select-option-group.json'; -import SelectOptionGroupLabelApiJsonPageContent from '../../api/select-option-group-label.json'; -import SelectOptionIndicatorApiJsonPageContent from '../../api/select-option-indicator.json'; -import SelectPopupApiJsonPageContent from '../../api/select-popup.json'; -import SelectPositionerApiJsonPageContent from '../../api/select-positioner.json'; -import SelectRootApiJsonPageContent from '../../api/select-root.json'; -import SelectScrollDownArrowApiJsonPageContent from '../../api/select-scroll-down-arrow.json'; -import SelectScrollUpArrowApiJsonPageContent from '../../api/select-scroll-up-arrow.json'; -import SelectSeparatorApiJsonPageContent from '../../api/select-separator.json'; -import SelectTriggerApiJsonPageContent from '../../api/select-trigger.json'; -import SelectValueApiJsonPageContent from '../../api/select-value.json'; - -export default function Page(props) { - const { userLanguage, ...other } = props; - return ; -} - -Page.getLayout = (page) => { - return {page}; -}; - -export const getStaticPaths = () => { - return { - paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }], - fallback: false, // can also be true or 'blocking' - }; -}; - -export const getStaticProps = () => { - const SelectArrowApiReq = require.context( - 'docs-base/translations/api-docs/select-arrow', - false, - /\.\/select-arrow.*.json$/, - ); - const SelectArrowApiDescriptions = mapApiPageTranslations(SelectArrowApiReq); - - const SelectBackdropApiReq = require.context( - 'docs-base/translations/api-docs/select-backdrop', - false, - /\.\/select-backdrop.*.json$/, - ); - const SelectBackdropApiDescriptions = mapApiPageTranslations(SelectBackdropApiReq); - - const SelectIconApiReq = require.context( - 'docs-base/translations/api-docs/select-icon', - false, - /\.\/select-icon.*.json$/, - ); - const SelectIconApiDescriptions = mapApiPageTranslations(SelectIconApiReq); - - const SelectOptionApiReq = require.context( - 'docs-base/translations/api-docs/select-option', - false, - /\.\/select-option.*.json$/, - ); - const SelectOptionApiDescriptions = mapApiPageTranslations(SelectOptionApiReq); - - const SelectOptionGroupApiReq = require.context( - 'docs-base/translations/api-docs/select-option-group', - false, - /\.\/select-option-group.*.json$/, - ); - const SelectOptionGroupApiDescriptions = mapApiPageTranslations(SelectOptionGroupApiReq); - - const SelectOptionGroupLabelApiReq = require.context( - 'docs-base/translations/api-docs/select-option-group-label', - false, - /\.\/select-option-group-label.*.json$/, - ); - const SelectOptionGroupLabelApiDescriptions = mapApiPageTranslations( - SelectOptionGroupLabelApiReq, - ); - - const SelectOptionIndicatorApiReq = require.context( - 'docs-base/translations/api-docs/select-option-indicator', - false, - /\.\/select-option-indicator.*.json$/, - ); - const SelectOptionIndicatorApiDescriptions = mapApiPageTranslations(SelectOptionIndicatorApiReq); - - const SelectPopupApiReq = require.context( - 'docs-base/translations/api-docs/select-popup', - false, - /\.\/select-popup.*.json$/, - ); - const SelectPopupApiDescriptions = mapApiPageTranslations(SelectPopupApiReq); - - const SelectPositionerApiReq = require.context( - 'docs-base/translations/api-docs/select-positioner', - false, - /\.\/select-positioner.*.json$/, - ); - const SelectPositionerApiDescriptions = mapApiPageTranslations(SelectPositionerApiReq); - - const SelectRootApiReq = require.context( - 'docs-base/translations/api-docs/select-root', - false, - /\.\/select-root.*.json$/, - ); - const SelectRootApiDescriptions = mapApiPageTranslations(SelectRootApiReq); - - const SelectScrollDownArrowApiReq = require.context( - 'docs-base/translations/api-docs/select-scroll-down-arrow', - false, - /\.\/select-scroll-down-arrow.*.json$/, - ); - const SelectScrollDownArrowApiDescriptions = mapApiPageTranslations(SelectScrollDownArrowApiReq); - - const SelectScrollUpArrowApiReq = require.context( - 'docs-base/translations/api-docs/select-scroll-up-arrow', - false, - /\.\/select-scroll-up-arrow.*.json$/, - ); - const SelectScrollUpArrowApiDescriptions = mapApiPageTranslations(SelectScrollUpArrowApiReq); - - const SelectSeparatorApiReq = require.context( - 'docs-base/translations/api-docs/select-separator', - false, - /\.\/select-separator.*.json$/, - ); - const SelectSeparatorApiDescriptions = mapApiPageTranslations(SelectSeparatorApiReq); - - const SelectTriggerApiReq = require.context( - 'docs-base/translations/api-docs/select-trigger', - false, - /\.\/select-trigger.*.json$/, - ); - const SelectTriggerApiDescriptions = mapApiPageTranslations(SelectTriggerApiReq); - - const SelectValueApiReq = require.context( - 'docs-base/translations/api-docs/select-value', - false, - /\.\/select-value.*.json$/, - ); - const SelectValueApiDescriptions = mapApiPageTranslations(SelectValueApiReq); - - return { - props: { - componentsApiDescriptions: { - SelectArrow: SelectArrowApiDescriptions, - SelectBackdrop: SelectBackdropApiDescriptions, - SelectIcon: SelectIconApiDescriptions, - SelectOption: SelectOptionApiDescriptions, - SelectOptionGroup: SelectOptionGroupApiDescriptions, - SelectOptionGroupLabel: SelectOptionGroupLabelApiDescriptions, - SelectOptionIndicator: SelectOptionIndicatorApiDescriptions, - SelectPopup: SelectPopupApiDescriptions, - SelectPositioner: SelectPositionerApiDescriptions, - SelectRoot: SelectRootApiDescriptions, - SelectScrollDownArrow: SelectScrollDownArrowApiDescriptions, - SelectScrollUpArrow: SelectScrollUpArrowApiDescriptions, - SelectSeparator: SelectSeparatorApiDescriptions, - SelectTrigger: SelectTriggerApiDescriptions, - SelectValue: SelectValueApiDescriptions, - }, - componentsApiPageContents: { - SelectArrow: SelectArrowApiJsonPageContent, - SelectBackdrop: SelectBackdropApiJsonPageContent, - SelectIcon: SelectIconApiJsonPageContent, - SelectOption: SelectOptionApiJsonPageContent, - SelectOptionGroup: SelectOptionGroupApiJsonPageContent, - SelectOptionGroupLabel: SelectOptionGroupLabelApiJsonPageContent, - SelectOptionIndicator: SelectOptionIndicatorApiJsonPageContent, - SelectPopup: SelectPopupApiJsonPageContent, - SelectPositioner: SelectPositionerApiJsonPageContent, - SelectRoot: SelectRootApiJsonPageContent, - SelectScrollDownArrow: SelectScrollDownArrowApiJsonPageContent, - SelectScrollUpArrow: SelectScrollUpArrowApiJsonPageContent, - SelectSeparator: SelectSeparatorApiJsonPageContent, - SelectTrigger: SelectTriggerApiJsonPageContent, - SelectValue: SelectValueApiJsonPageContent, - }, - hooksApiDescriptions: {}, - hooksApiPageContents: {}, - }, - }; -}; From 2ba2430c3098edb82dfc3231d5b8b4f43bab224f Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 6 Sep 2024 17:06:11 +1000 Subject: [PATCH 46/94] Fix caret based on direction --- packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index 952c71b07..0e4717c32 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -45,7 +45,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( (externalProps = {}) => mergeReactProps<'div'>(externalProps, { 'aria-hidden': true, - children: '▼', + children: direction === 'down' ? '▼' : '▲', style: { position: 'absolute', zIndex: 2147483647, // max z-index From b1109f92fc92f5b5d9aaf81db83286efcbfbd485 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 9 Sep 2024 16:36:31 +1000 Subject: [PATCH 47/94] Misc fixes, docs updates --- docs/data/api/select-root.json | 4 +- docs/data/components/select/SelectEmpty.js | 162 ++++++++++++++ docs/data/components/select/SelectEmpty.tsx | 162 ++++++++++++++ docs/data/components/select/SelectGroup.js | 207 ++++++++++++++++++ docs/data/components/select/SelectGroup.tsx | 207 ++++++++++++++++++ .../select/SelectIntroduction/system/index.js | 117 ++-------- .../SelectIntroduction/system/index.tsx | 115 ++-------- docs/data/components/select/SelectSimple.js | 158 +++++++++++++ docs/data/components/select/select.mdx | 76 ++++++- docs/next-env.d.ts | 1 - .../src/Select/Option/SelectOption.tsx | 9 +- .../OptionIndicator/SelectOptionIndicator.tsx | 1 + .../Select/OptionText/SelectOptionText.tsx | 10 +- .../Select/Positioner/SelectPositioner.tsx | 26 +-- .../mui-base/src/Select/Root/SelectRoot.tsx | 10 +- .../src/Select/Root/useSelectRoot.tsx | 16 +- .../Select/ScrollArrow/SelectScrollArrow.tsx | 62 +++--- 17 files changed, 1066 insertions(+), 277 deletions(-) create mode 100644 docs/data/components/select/SelectEmpty.js create mode 100644 docs/data/components/select/SelectEmpty.tsx create mode 100644 docs/data/components/select/SelectGroup.js create mode 100644 docs/data/components/select/SelectGroup.tsx create mode 100644 docs/data/components/select/SelectSimple.js diff --git a/docs/data/api/select-root.json b/docs/data/api/select-root.json index 7db5208bb..88a23449a 100644 --- a/docs/data/api/select-root.json +++ b/docs/data/api/select-root.json @@ -6,7 +6,7 @@ }, "animated": { "type": { "name": "bool" }, "default": "true" }, "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, - "defaultValue": { "type": { "name": "string" } }, + "defaultValue": { "type": { "name": "any" } }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "id": { "type": { "name": "string" } }, "loop": { "type": { "name": "bool" }, "default": "true" }, @@ -16,7 +16,7 @@ "open": { "type": { "name": "bool" } }, "readOnly": { "type": { "name": "bool" }, "default": "false" }, "required": { "type": { "name": "bool" }, "default": "false" }, - "value": { "type": { "name": "string" } } + "value": { "type": { "name": "any" } } }, "name": "SelectRoot", "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectRoot = Select.Root;"], diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js new file mode 100644 index 000000000..7c455a652 --- /dev/null +++ b/docs/data/components/select/SelectEmpty.js @@ -0,0 +1,162 @@ +'use client'; + +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; +import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; + +export default function SelectEmpty() { + return ( + + + + + + + + +
            + +
            +
            + + + } /> + Select font... + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + +
            + +
            +
            +
            +
            + ); +} + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: calc(var(--anchor-width) + 20px); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + top: 0; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } +`; diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx new file mode 100644 index 000000000..7c455a652 --- /dev/null +++ b/docs/data/components/select/SelectEmpty.tsx @@ -0,0 +1,162 @@ +'use client'; + +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; +import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; + +export default function SelectEmpty() { + return ( + + + + + + + + +
            + +
            +
            + + + } /> + Select font... + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + +
            + +
            +
            +
            +
            + ); +} + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: calc(var(--anchor-width) + 20px); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + top: 0; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } +`; diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js new file mode 100644 index 000000000..c5b033b08 --- /dev/null +++ b/docs/data/components/select/SelectGroup.js @@ -0,0 +1,207 @@ +'use client'; + +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; +import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; + +function createOptions(items) { + return items.map((item) => ({ + value: item, + label: item[0].toUpperCase() + item.slice(1), + })); +} + +const data = { + Fruits: createOptions(['apple', 'banana', 'orange', 'pear', 'grape', 'pineapple']), + Vegetables: createOptions([ + 'carrot', + 'lettuce', + 'broccoli', + 'cauliflower', + 'asparagus', + 'zucchini', + ]), +}; + +const entries = Object.entries(data); + +export default function SelectGroup() { + return ( + + + + + + + + +
            + +
            +
            + + + } /> + Select food... + + {entries.map(([group, items]) => ( + + + + {group} + {items.map((item) => ( + + } /> + {item.label} + + ))} + + + ))} + + +
            + +
            +
            +
            +
            + ); +} + +const gray = { + 300: '#e5e7eb', +}; + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: calc(var(--anchor-width) + 20px); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` + font-weight: bold; + padding: 4px 24px; + cursor: default; + user-select: none; + height: 30px; +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + top: 0; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } +`; + +const SelectSeparator = styled(Select.Separator)` + height: 1px; + background-color: ${gray[300]}; + margin: 5px 0; +`; diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx new file mode 100644 index 000000000..b9865f349 --- /dev/null +++ b/docs/data/components/select/SelectGroup.tsx @@ -0,0 +1,207 @@ +'use client'; + +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; +import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; + +function createOptions(items: string[]) { + return items.map((item) => ({ + value: item, + label: item[0].toUpperCase() + item.slice(1), + })); +} + +const data = { + Fruits: createOptions(['apple', 'banana', 'orange', 'pear', 'grape', 'pineapple']), + Vegetables: createOptions([ + 'carrot', + 'lettuce', + 'broccoli', + 'cauliflower', + 'asparagus', + 'zucchini', + ]), +}; + +const entries = Object.entries(data); + +export default function SelectGroup() { + return ( + + + + + + + + +
            + +
            +
            + + + } /> + Select food... + + {entries.map(([group, items]) => ( + + + + {group} + {items.map((item) => ( + + } /> + {item.label} + + ))} + + + ))} + + +
            + +
            +
            +
            +
            + ); +} + +const gray = { + 300: '#e5e7eb', +}; + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: calc(var(--anchor-width) + 20px); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` + font-weight: bold; + padding: 4px 24px; + cursor: default; + user-select: none; + height: 30px; +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + top: 0; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } +`; + +const SelectSeparator = styled(Select.Separator)` + height: 1px; + background-color: ${gray[300]}; + margin: 5px 0; +`; diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 217a28495..41c889358 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -6,100 +6,33 @@ import { css, styled } from '@mui/system'; import Check from '@mui/icons-material/Check'; import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; -const data = { - Fruits: [ - { - value: 'apple', - label: 'Apple', - }, - { - value: 'banana', - label: 'Banana', - }, - { - value: 'orange', - label: 'Orange', - }, - { - value: 'pear', - label: 'Pear', - }, - { - value: 'grape', - label: 'Grape', - }, - { - value: 'pineapple', - label: 'Pineapple', - }, - ], - Vegetables: [ - { - value: 'carrot', - label: 'Carrot', - }, - { - value: 'lettuce', - label: 'Lettuce', - }, - { - value: 'broccoli', - label: 'Broccoli', - }, - { - value: 'cauliflower', - label: 'Cauliflower', - }, - { - value: 'asparagus', - label: 'Asparagus', - }, - { - value: 'zucchini', - label: 'Zucchini', - }, - ], -}; - -const entries = Object.entries(data); - -export default function UnstyledSelectIntroduction() { +export default function SelectSimple() { return ( - - - + + + - +
            - + } /> - Select food... + System font + + + } /> + Arial + + + } /> + Roboto - {entries.map(([group, items]) => ( - - - - {group} - {items.map((item) => ( - - } /> - {item.label} - - ))} - - - ))}
            @@ -111,10 +44,6 @@ export default function UnstyledSelectIntroduction() { ); } -const gray = { - 300: '#e5e7eb', -}; - const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; @@ -128,6 +57,7 @@ const SelectTrigger = styled(Select.Trigger)` font-size: 100%; line-height: 1.5; user-select: none; + cursor: default; &:focus-visible { outline: 2px solid black; @@ -151,7 +81,7 @@ const SelectPopup = styled(Select.Popup)` 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 16px); + min-width: calc(var(--anchor-width) + 20px); `; const SelectOption = styled(Select.Option)` @@ -187,13 +117,6 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` } `; -const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` - font-weight: bold; - padding: 4px 24px; - cursor: default; - user-select: none; -`; - const scrollArrowStyles = css` width: 100%; height: 15px; @@ -233,9 +156,3 @@ const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` bottom: -10px; } `; - -const SelectSeparator = styled(Select.Separator)` - height: 1px; - background-color: ${gray[300]}; - margin: 5px 0; -`; diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 25ceab09e..41c889358 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -6,100 +6,33 @@ import { css, styled } from '@mui/system'; import Check from '@mui/icons-material/Check'; import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; -const data = { - Fruits: [ - { - value: 'apple', - label: 'Apple', - }, - { - value: 'banana', - label: 'Banana', - }, - { - value: 'orange', - label: 'Orange', - }, - { - value: 'pear', - label: 'Pear', - }, - { - value: 'grape', - label: 'Grape', - }, - { - value: 'pineapple', - label: 'Pineapple', - }, - ], - Vegetables: [ - { - value: 'carrot', - label: 'Carrot', - }, - { - value: 'lettuce', - label: 'Lettuce', - }, - { - value: 'broccoli', - label: 'Broccoli', - }, - { - value: 'cauliflower', - label: 'Cauliflower', - }, - { - value: 'asparagus', - label: 'Asparagus', - }, - { - value: 'zucchini', - label: 'Zucchini', - }, - ], -}; - -const entries = Object.entries(data); - -export default function UnstyledSelectIntroduction() { +export default function SelectSimple() { return ( - - - + + + - +
            - + } /> - Select food... + System font + + + } /> + Arial + + + } /> + Roboto - {entries.map(([group, items]) => ( - - - - {group} - {items.map((item) => ( - - } /> - {item.label} - - ))} - - - ))}
            @@ -111,10 +44,6 @@ export default function UnstyledSelectIntroduction() { ); } -const gray = { - 300: '#e5e7eb', -}; - const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; @@ -128,6 +57,7 @@ const SelectTrigger = styled(Select.Trigger)` font-size: 100%; line-height: 1.5; user-select: none; + cursor: default; &:focus-visible { outline: 2px solid black; @@ -187,13 +117,6 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` } `; -const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` - font-weight: bold; - padding: 4px 24px; - cursor: default; - user-select: none; -`; - const scrollArrowStyles = css` width: 100%; height: 15px; @@ -233,9 +156,3 @@ const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` bottom: -10px; } `; - -const SelectSeparator = styled(Select.Separator)` - height: 1px; - background-color: ${gray[300]}; - margin: 5px 0; -`; diff --git a/docs/data/components/select/SelectSimple.js b/docs/data/components/select/SelectSimple.js new file mode 100644 index 000000000..41c889358 --- /dev/null +++ b/docs/data/components/select/SelectSimple.js @@ -0,0 +1,158 @@ +'use client'; + +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; +import Check from '@mui/icons-material/Check'; +import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; + +export default function SelectSimple() { + return ( + + + + + + + + +
            + +
            +
            + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + +
            + +
            +
            +
            +
            + ); +} + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + background-color: white; + padding: 4px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: calc(var(--anchor-width) + 20px); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + top: 0; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + transform: rotate(180deg); + top: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + bottom: 0; + ${scrollArrowStyles} + + &[data-side='none'] { + bottom: -10px; + } +`; diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index c8cae915d..fde677fa0 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -1,6 +1,7 @@ --- productId: base-ui -title: React Select components and hook +title: React Select component +description: Select provides users with a floating element containing a list of options to choose from. components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionText, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ @@ -71,6 +72,70 @@ Selects are implemented using a collection of related components: ``` +## Default value + +To set an initial value when uncontrolled, use `defaultValue`: + +```jsx + +``` + +## Empty value + +The select's value is empty (`""`) by default, which enables an empty state: + + + +## Controlled + +To control the value with external state, specify the `value` and `onValueChange` props: + +```jsx +const [value, setValue] = React.useState('system'); + +return ( + + {/* subcomponents */} + +); +``` + +## Option indicator + +The `Select.OptionIndicator` subcomponent renders an indicator inside an option to indicate it's selected. By default, it renders a check icon, but this can be customized: + +```jsx + + + +``` + +The `[data-selected]` attribute is added to the subcomponent when its owning option is selected. This is necessary to hide the indicator when the option is not selected: + +```css +.SelectOptionIndicator { + visibility: hidden; +} + +.SelectOptionIndicator[data-selected] { + visibility: visible; +} +``` + +## Grouped options + +`Select.OptionGroup` can be used to group options together with a label. The `Select.OptionGroupLabel` subcomponent renders the label: + +```jsx + + Label + Option 1 + Option 2 + +``` + + + ## Align method Two different methods to align the popup are available: @@ -82,11 +147,10 @@ Two different methods to align the popup are available: ``` -The `item` method aligns the popup such that the selected item inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. - -The `trigger` method aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. +- **`item`**: aligns the popup such that the selected item inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. This method is useful as it allows the user to select the an item in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS. The scroll arrow components must be used to ensure this is the case. +- **`trigger`**: aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. -## Value +## Value component The `Select.Value` subcomponent renders the selected value. This is the text content or `label` of `Select.Option` by default. @@ -101,5 +165,5 @@ The `placeholder` prop can be used when the value is empty. During SSR, if a def A function can be specified as a child to customize the rendering of the value: ```jsx -{(value) => {value}} +{(value) => value.toLowerCase()} ``` diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index fd36f9494..4f11a03dc 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/mui-base/src/Select/Option/SelectOption.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx index c322facce..4fdb4d156 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { UseInteractionsReturn, useListItem } from '@floating-ui/react'; +import type { UseInteractionsReturn } from '@floating-ui/react'; import { useSelectOption } from './useSelectOption'; import { SelectRootContext, useSelectRootContext } from '../Root/SelectRootContext'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; @@ -12,6 +12,7 @@ import { useEventCallback } from '../../utils/useEventCallback'; import { SelectOptionContext } from './SelectOptionContext'; import { commonStyleHooks } from '../utils/commonStyleHooks'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; +import { useCompositeListItem } from '../../Composite/List/useCompositeListItem'; const InnerSelectOption = React.memo( React.forwardRef(function InnerSelectOption( @@ -101,10 +102,8 @@ const SelectOption = React.forwardRef(function SelectOption( valuesRef, } = useSelectRootContext(); - const [item, setItem] = React.useState(null); - const itemLabel = label ?? item?.textContent ?? null; - const listItem = useListItem({ label: itemLabel }); - const mergedRef = useForkRef(forwardedRef, listItem.ref, setItem); + const listItem = useCompositeListItem({ label }); + const mergedRef = useForkRef(forwardedRef, listItem.ref); useEnhancedEffect(() => { if (listItem.index === -1) { diff --git a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx index 48b470759..bb0390b84 100644 --- a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx @@ -34,6 +34,7 @@ const SelectOptionIndicator = React.forwardRef(function SelectOptionIndicator( (externalProps = {}) => mergeReactProps(externalProps, { 'aria-hidden': true, + children: '✔️', }), [], ); diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index f394501c2..dba6c8518 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -24,22 +24,18 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( ) { const { className, render, ...otherProps } = props; - const { open, triggerElement, valueRef, popupRef, touchModality, innerFallback } = - useSelectRootContext(); + const { open, triggerElement, valueRef, popupRef, innerFallback } = useSelectRootContext(); const { isPositioned, setOptionTextOffset } = useSelectPositionerContext(); const { selected } = useSelectOptionContext(); - const inert = touchModality || innerFallback; - const textRef = React.useRef(null); - const mergedRef = useForkRef(forwardedRef, textRef); const ownerState: SelectOptionText.OwnerState = React.useMemo(() => ({}), []); useEnhancedEffect(() => { if ( - inert || + innerFallback || !open || !isPositioned || !selected || @@ -61,7 +57,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( setOptionTextOffset(triggerDiff - popupDiff); }, [ - inert, + innerFallback, open, isPositioned, popupRef, diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index a5ca48a00..e4094c353 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -1,13 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { - FloatingFocusManager, - FloatingList, - FloatingPortal, - inner, - type Side, -} from '@floating-ui/react'; +import { FloatingFocusManager, FloatingPortal, inner, type Side } from '@floating-ui/react'; import type { BaseUIComponentProps } from '../../utils/types'; import { SelectPositionerContext } from './SelectPositionerContext'; import { useSelectRootContext } from '../Root/SelectRootContext'; @@ -22,6 +16,7 @@ import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useId } from '../../utils/useId'; import { useLatestRef } from '../../utils/useLatestRef'; import { mergeReactProps } from '../../utils/mergeReactProps'; +import { CompositeList } from '../../Composite/List/CompositeList'; /** * Renders the element that positions the Select popup. @@ -97,7 +92,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const [optionTextOffset, setOptionTextOffset] = React.useState(null); const [selectedIndexOnMount, setSelectedIndexOnMount] = React.useState(selectedIndex); - if (optionTextOffset !== null && (!mounted || innerFallback || touchModality)) { + if (optionTextOffset !== null && (!mounted || innerFallback)) { setOptionTextOffset(null); } @@ -213,6 +208,11 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const positionerElement = renderElement(); + const stringValue = React.useMemo( + () => (typeof value === 'string' ? value : JSON.stringify(value)), + [value], + ); + const mountedItemsElement = keepMounted ? null : ; const nativeSelectElement = ( ); @@ -251,17 +251,17 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( if (!shouldRender) { return ( - + {nativeSelectElement} {mountedItemsElement} - + ); } return ( - + {nativeSelectElement} - + ); }); diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index dff91f4a7..a3fd920b1 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -98,15 +98,15 @@ namespace SelectRoot { /** * The value of the select. */ - value?: string; + value?: unknown; /** * Callback fired when the value of the select changes. Use when controlled. */ - onValueChange?: (value: string, event?: Event) => void; + onValueChange?: (value: unknown, event?: Event) => void; /** * The default value of the select. */ - defaultValue?: string; + defaultValue?: unknown; /** * If `true`, the Select is initially open. * @@ -167,7 +167,7 @@ SelectRoot.propTypes /* remove-proptypes */ = { /** * The default value of the select. */ - defaultValue: PropTypes.string, + defaultValue: PropTypes.any, /** * If `true`, the Select is disabled. * @@ -213,7 +213,7 @@ SelectRoot.propTypes /* remove-proptypes */ = { /** * The value of the select. */ - value: PropTypes.string, + value: PropTypes.any, } as any; export { SelectRoot }; diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 78d483349..dd90c181d 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -60,7 +60,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const typingRef = React.useRef(false); const elementsRef = React.useRef>([]); const labelsRef = React.useRef>([]); - const valuesRef = React.useRef>([]); + const valuesRef = React.useRef>([]); const selectionRef = React.useRef({ allowMouseUp: false, allowSelect: false }); const overflowRef = React.useRef({ top: 0, bottom: 0, left: 0, right: 0 }); const valueRef = React.useRef(null); @@ -304,16 +304,16 @@ export namespace useSelectRoot { /** * The value of the Select. Use when controlled. */ - value?: string; + value?: unknown; /** * Callback fired when the value of the Select changes. Use when controlled. */ - onValueChange?: (value: string, event?: Event) => void; + onValueChange?: (value: unknown, event?: Event) => void; /** * The default value of the Select. * @default '' */ - defaultValue?: string; + defaultValue?: unknown; /** * Determines if the select should align to the selected item inside the popup or the trigger * element. @@ -323,8 +323,8 @@ export namespace useSelectRoot { } export interface ReturnValue extends useFieldControlValidation.ReturnValue { - value: string; - setValue: (value: string, event?: Event) => void; + value: unknown; + setValue: (value: unknown, event?: Event) => void; label: string; setLabel: React.Dispatch>; activeIndex: number | null; @@ -336,8 +336,8 @@ export namespace useSelectRoot { getPopupProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; elementsRef: React.MutableRefObject<(HTMLElement | null)[]>; - labelsRef: React.MutableRefObject<(string | null)[]>; - valuesRef: React.MutableRefObject<(string | null)[]>; + labelsRef: React.MutableRefObject>; + valuesRef: React.MutableRefObject>; mounted: boolean; open: boolean; popupRef: React.RefObject; diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index 0e4717c32..70d845435 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -22,12 +22,12 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( useSelectRootContext(); const { isPositioned, side } = useSelectPositionerContext(); - const [rendered, setRendered] = React.useState(false); + const [visible, setVisible] = React.useState(false); const inert = alignMethod === 'trigger' || touchModality; - if (rendered && inert) { - setRendered(false); + if (visible && inert) { + setVisible(false); } const frameRef = React.useRef(-1); @@ -35,10 +35,10 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( const ownerState: SelectScrollArrow.OwnerState = React.useMemo( () => ({ direction, - rendered, + visible, side, }), - [direction, rendered, side], + [direction, visible, side], ); const getScrollArrowProps = React.useCallback( @@ -58,7 +58,8 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( let prevNow = Date.now(); function handleFrame() { - if (!popupRef.current) { + const popupElement = popupRef.current; + if (!popupElement) { return; } @@ -66,23 +67,22 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( const msElapsed = currentNow - prevNow; prevNow = currentNow; - const pixelsToScroll = Math.floor( - Math.min( - msElapsed / 2, - popupRef.current.scrollHeight - popupRef.current.clientHeight, - ) || 1, - ); + const pixelsLeftToScroll = + direction === 'up' + ? popupElement.scrollTop + : popupElement.scrollHeight - popupElement.clientHeight - popupElement.scrollTop; + const pixelsToScroll = Math.min(pixelsLeftToScroll, msElapsed / 2); - const isScrolledToTop = popupRef.current.scrollTop === 0; + const isScrolledToTop = popupElement.scrollTop === 0; const isScrolledToBottom = - Math.ceil(popupRef.current.scrollTop + popupRef.current.clientHeight) >= - popupRef.current.scrollHeight; + Math.round(popupElement.scrollTop + popupElement.clientHeight) >= + popupElement.scrollHeight; if (msElapsed > 0) { if (direction === 'up') { - setRendered(!isScrolledToTop); + setVisible(!isScrolledToTop); } else if (direction === 'down') { - setRendered(!isScrolledToBottom); + setVisible(!isScrolledToBottom); } if ( @@ -114,19 +114,19 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( [direction, innerFallback, popupRef, setInnerOffset, inert], ); - const handleScrollArrowRendered = useEventCallback(() => { + const handleScrollArrowVisible = useEventCallback(() => { const popupElement = popupRef.current; if (!popupElement) { return; } if (direction === 'up') { - setRendered(popupElement.scrollTop > 1); + setVisible(popupElement.scrollTop > 1); } else if (direction === 'down') { const isScrolledToBottom = Math.ceil(popupElement.scrollTop + popupElement.clientHeight) >= popupElement.scrollHeight - 1; - setRendered(!isScrolledToBottom); + setVisible(!isScrolledToBottom); } }); @@ -138,24 +138,24 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( const win = ownerWindow(popupElement); - popupElement.addEventListener('wheel', handleScrollArrowRendered); - popupElement.addEventListener('scroll', handleScrollArrowRendered); - win.addEventListener('resize', handleScrollArrowRendered); + popupElement.addEventListener('wheel', handleScrollArrowVisible); + popupElement.addEventListener('scroll', handleScrollArrowVisible); + win.addEventListener('resize', handleScrollArrowVisible); return () => { - popupElement.removeEventListener('wheel', handleScrollArrowRendered); - popupElement.removeEventListener('scroll', handleScrollArrowRendered); - win.removeEventListener('resize', handleScrollArrowRendered); + popupElement.removeEventListener('wheel', handleScrollArrowVisible); + popupElement.removeEventListener('scroll', handleScrollArrowVisible); + win.removeEventListener('resize', handleScrollArrowVisible); }; - }, [inert, popupRef, direction, handleScrollArrowRendered]); + }, [inert, popupRef, direction, handleScrollArrowVisible]); useEnhancedEffect(() => { if (!isPositioned || inert) { return; } - handleScrollArrowRendered(); - }, [isPositioned, innerOffset, inert, handleScrollArrowRendered]); + handleScrollArrowVisible(); + }, [isPositioned, side, innerOffset, inert, handleScrollArrowVisible]); const { renderElement } = useComponentRenderer({ propGetter: getScrollArrowProps, @@ -166,7 +166,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( extraProps: otherProps, }); - const shouldRender = rendered || keepMounted; + const shouldRender = visible || keepMounted; if (!shouldRender) { return null; } @@ -178,7 +178,7 @@ namespace SelectScrollArrow { export interface OwnerState { direction: 'up' | 'down'; side: 'top' | 'right' | 'bottom' | 'left' | 'none'; - rendered: boolean; + visible: boolean; } export interface Props extends BaseUIComponentProps<'div', OwnerState> { direction: 'up' | 'down'; From ee6309e54b4b6b4c403ab57d3431d3be73ef62a0 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 9 Sep 2024 16:56:09 +1000 Subject: [PATCH 48/94] Fix type --- packages/mui-base/src/Select/Root/SelectRoot.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index 649b83b29..504b97338 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -122,7 +122,7 @@ describe('', () => { const handleValueChange = spy(); function App() { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState(''); return ( Date: Mon, 9 Sep 2024 17:05:23 +1000 Subject: [PATCH 49/94] Fix test dupes --- .../src/Field/Root/FieldRoot.test.tsx | 67 ------------------- 1 file changed, 67 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 65f811db8..f6043593a 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -275,27 +275,6 @@ describe('', () => { expect(trigger).to.have.attribute('aria-invalid', 'true'); }); - - it('supports RadioGroup', async () => { - await render( - 'error'}> - - One - Two - - - , - ); - - const group = screen.getByTestId('group'); - - expect(group).not.to.have.attribute('aria-invalid'); - - fireEvent.focus(group); - fireEvent.blur(group); - - expect(group).to.have.attribute('aria-invalid', 'true'); - }); }); }); @@ -541,33 +520,6 @@ describe('', () => { expect(group).to.have.attribute('data-touched', 'true'); expect(control).to.have.attribute('data-touched', 'true'); }); - - it('supports Select', async () => { - render( - - - - - - Select - Option 1 - - - - , - ); - - const trigger = screen.getByTestId('trigger'); - - expect(trigger).not.to.have.attribute('data-dirty'); - - fireEvent.focus(trigger); - fireEvent.blur(trigger); - - await flushMicrotasks(); - - expect(trigger).to.have.attribute('data-touched', 'true'); - }); }); describe('dirty', () => { @@ -731,25 +683,6 @@ describe('', () => { expect(trigger).to.have.attribute('data-dirty', 'true'); }); - - it('supports RadioGroup', async () => { - await render( - - - One - Two - - , - ); - - const group = screen.getByTestId('group'); - - expect(group).not.to.have.attribute('data-dirty'); - - fireEvent.click(screen.getByText('One')); - - expect(group).to.have.attribute('data-dirty', 'true'); - }); }); }); }); From 12963d061beaf194fa69e50ffe2ad7c774155848 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 9 Sep 2024 18:38:58 +1000 Subject: [PATCH 50/94] Fix test assertion --- packages/mui-base/src/Select/Root/SelectRoot.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index 504b97338..4faeda345 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -200,7 +200,7 @@ describe('', () => { const { rerender } = await render(); - expect(screen.queryByRole('listbox')).not.to.equal(null); + expect(screen.queryByRole('listbox')).to.equal(null); rerender(); From 6161640da64da403e9aa2e7703f9d0cc294208e8 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 9 Sep 2024 18:39:04 +1000 Subject: [PATCH 51/94] Fix useInnerOffset enabled check --- packages/mui-base/src/Select/Root/useSelectRoot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index dd90c181d..7ce7bbd05 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -185,7 +185,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R }); const innerOffsetInteractionProps = useInnerOffset(floatingRootContext, { - enabled: alignMethod && !innerFallback, + enabled: alignMethod === 'item' && !innerFallback, onChange: setInnerOffset, scrollRef: popupRef, overflowRef, From 4f73953da5cfd6222dad2018f52aef90dcd10dd4 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 9 Sep 2024 18:40:45 +1000 Subject: [PATCH 52/94] Fix tests --- packages/mui-base/src/Field/Root/FieldRoot.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index f6043593a..cc4255436 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -233,8 +233,8 @@ describe('', () => { expect(input).to.have.attribute('aria-invalid', 'true'); }); - it('supports RadioGroup', () => { - render( + it('supports RadioGroup', async () => { + await render( 'error'}> One @@ -630,8 +630,8 @@ describe('', () => { expect(root).to.have.attribute('data-dirty', 'true'); }); - it('supports RadioGroup', () => { - render( + it('supports RadioGroup', async () => { + await render( One From ae5f9e7017b87cdcc6f795f673b3566186441c58 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 10 Sep 2024 15:44:27 +1000 Subject: [PATCH 53/94] API updates --- docs/data/api/select-group-label.json | 19 ++++++++++++ docs/data/api/select-group.json | 19 ++++++++++++ docs/data/api/select-option.json | 4 +-- docs/data/components/select/SelectEmpty.js | 2 +- docs/data/components/select/SelectEmpty.tsx | 2 +- docs/data/components/select/SelectGroup.js | 8 ++--- docs/data/components/select/SelectGroup.tsx | 8 ++--- docs/data/components/select/select.mdx | 2 +- .../select-group-label.json} | 0 .../select-group.json} | 0 .../SelectGroup.test.tsx} | 20 ++++++------- .../SelectGroup.tsx} | 22 +++++++------- .../SelectGroupContext.ts} | 8 ++--- .../SelectGroupLabel.test.tsx} | 6 ++-- .../SelectGroupLabel.tsx} | 19 ++++++------ .../src/Select/Option/SelectOption.tsx | 4 +-- .../mui-base/src/Select/Popup/SelectPopup.tsx | 1 + .../src/Select/Popup/useSelectPopup.ts | 30 +++++++++++++++++-- .../Select/Positioner/SelectPositioner.tsx | 15 ++++++---- packages/mui-base/src/Select/index.barrel.ts | 4 +-- packages/mui-base/src/Select/index.ts | 4 +-- 21 files changed, 133 insertions(+), 64 deletions(-) create mode 100644 docs/data/api/select-group-label.json create mode 100644 docs/data/api/select-group.json rename docs/data/translations/api-docs/{select-option-group-label/select-option-group-label.json => select-group-label/select-group-label.json} (100%) rename docs/data/translations/api-docs/{select-option-group/select-option-group.json => select-group/select-group.json} (100%) rename packages/mui-base/src/Select/{OptionGroup/SelectOptionGroup.test.tsx => Group/SelectGroup.test.tsx} (73%) rename packages/mui-base/src/Select/{OptionGroup/SelectOptionGroup.tsx => Group/SelectGroup.tsx} (78%) rename packages/mui-base/src/Select/{OptionGroup/SelectOptionGroupContext.ts => Group/SelectGroupContext.ts} (52%) rename packages/mui-base/src/Select/{OptionGroupLabel/SelectOptionGroupLabel.test.tsx => GroupLabel/SelectGroupLabel.test.tsx} (68%) rename packages/mui-base/src/Select/{OptionGroupLabel/SelectOptionGroupLabel.tsx => GroupLabel/SelectGroupLabel.tsx} (80%) diff --git a/docs/data/api/select-group-label.json b/docs/data/api/select-group-label.json new file mode 100644 index 000000000..fc820da7a --- /dev/null +++ b/docs/data/api/select-group-label.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
            | string" } }, + "render": { "type": { "name": "union", "description": "element
            | func" } } + }, + "name": "SelectGroupLabel", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectGroupLabel = Select.GroupLabel;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectGroupLabel", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/data/api/select-group.json b/docs/data/api/select-group.json new file mode 100644 index 000000000..9895f3044 --- /dev/null +++ b/docs/data/api/select-group.json @@ -0,0 +1,19 @@ +{ + "props": { + "className": { "type": { "name": "union", "description": "func
            | string" } }, + "render": { "type": { "name": "union", "description": "element
            | func" } } + }, + "name": "SelectGroup", + "imports": [ + "import * as Select from '@base_ui/react/Select';\nconst SelectGroup = Select.Group;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "SelectGroup", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Select/Group/SelectGroup.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/data/api/select-option.json b/docs/data/api/select-option.json index 7bf8537d2..dd2f64a59 100644 --- a/docs/data/api/select-option.json +++ b/docs/data/api/select-option.json @@ -1,9 +1,9 @@ { "props": { - "value": { "type": { "name": "string" }, "required": true }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "label": { "type": { "name": "string" } }, - "onClick": { "type": { "name": "func" } } + "onClick": { "type": { "name": "func" } }, + "value": { "type": { "name": "any" } } }, "name": "SelectOption", "imports": [ diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js index 7c455a652..7fa7db27f 100644 --- a/docs/data/components/select/SelectEmpty.js +++ b/docs/data/components/select/SelectEmpty.js @@ -21,7 +21,7 @@ export default function SelectEmpty() {
            - + } /> Select font... diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx index 7c455a652..7fa7db27f 100644 --- a/docs/data/components/select/SelectEmpty.tsx +++ b/docs/data/components/select/SelectEmpty.tsx @@ -21,7 +21,7 @@ export default function SelectEmpty() {
            - + } /> Select font... diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js index c5b033b08..d560efca7 100644 --- a/docs/data/components/select/SelectGroup.js +++ b/docs/data/components/select/SelectGroup.js @@ -49,8 +49,8 @@ export default function SelectGroup() { {entries.map(([group, items]) => ( - - {group} + + {group} {items.map((item) => ( {item.label}
            ))} -
            +
            ))}
            @@ -152,7 +152,7 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` } `; -const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` +const SelectGroupLabel = styled(Select.GroupLabel)` font-weight: bold; padding: 4px 24px; cursor: default; diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx index b9865f349..1d9b3c6ef 100644 --- a/docs/data/components/select/SelectGroup.tsx +++ b/docs/data/components/select/SelectGroup.tsx @@ -49,8 +49,8 @@ export default function SelectGroup() { {entries.map(([group, items]) => ( - - {group} + + {group} {items.map((item) => ( {item.label} ))} - + ))} @@ -152,7 +152,7 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` } `; -const SelectOptionGroupLabel = styled(Select.OptionGroupLabel)` +const SelectGroupLabel = styled(Select.GroupLabel)` font-weight: bold; padding: 4px 24px; cursor: default; diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index fde677fa0..8e7d55a5d 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -2,7 +2,7 @@ productId: base-ui title: React Select component description: Select provides users with a floating element containing a list of options to choose from. -components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionText, SelectOptionIndicator, SelectOptionGroup, SelectOptionGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow +components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionText, SelectOptionIndicator, SelectGroup, SelectGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- diff --git a/docs/data/translations/api-docs/select-option-group-label/select-option-group-label.json b/docs/data/translations/api-docs/select-group-label/select-group-label.json similarity index 100% rename from docs/data/translations/api-docs/select-option-group-label/select-option-group-label.json rename to docs/data/translations/api-docs/select-group-label/select-group-label.json diff --git a/docs/data/translations/api-docs/select-option-group/select-option-group.json b/docs/data/translations/api-docs/select-group/select-group.json similarity index 100% rename from docs/data/translations/api-docs/select-option-group/select-option-group.json rename to docs/data/translations/api-docs/select-group/select-group.json diff --git a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx b/packages/mui-base/src/Select/Group/SelectGroup.test.tsx similarity index 73% rename from packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx rename to packages/mui-base/src/Select/Group/SelectGroup.test.tsx index 34df3009b..4d7ddbf4c 100644 --- a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.test.tsx +++ b/packages/mui-base/src/Select/Group/SelectGroup.test.tsx @@ -4,10 +4,10 @@ import { createRenderer, describeConformance } from '#test-utils'; import { screen } from '@mui/internal-test-utils'; import { expect } from 'chai'; -describe('', () => { +describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( @@ -22,11 +22,11 @@ describe('', () => { await render( - - Fruits + + Fruits Apple Banana - + , ); @@ -39,17 +39,17 @@ describe('', () => { await render( - - Vegetables + + Vegetables Carrot Lettuce - + , ); - const optionGroup = screen.getByRole('group'); + const Group = screen.getByRole('group'); const label = screen.getByText('Vegetables'); - expect(optionGroup).to.have.attribute('aria-labelledby', label.id); + expect(Group).to.have.attribute('aria-labelledby', label.id); }); }); diff --git a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx b/packages/mui-base/src/Select/Group/SelectGroup.tsx similarity index 78% rename from packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx rename to packages/mui-base/src/Select/Group/SelectGroup.tsx index d59d0b701..8eced6fc2 100644 --- a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroup.tsx +++ b/packages/mui-base/src/Select/Group/SelectGroup.tsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; -import { SelectOptionGroupContext } from './SelectOptionGroupContext'; +import { SelectGroupContext } from './SelectGroupContext'; import { useSelectRootContext } from '../Root/SelectRootContext'; /** @@ -15,10 +15,10 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; * * API: * - * - [SelectOptionGroup API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOptionGroup) + * - [SelectGroup API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectGroup) */ -const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( - props: SelectOptionGroup.Props, +const SelectGroup = React.forwardRef(function SelectGroup( + props: SelectGroup.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, ...otherProps } = props; @@ -27,7 +27,7 @@ const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( const [labelId, setLabelId] = React.useState(); - const ownerState: SelectOptionGroup.OwnerState = React.useMemo( + const ownerState: SelectGroup.OwnerState = React.useMemo( () => ({ open, }), @@ -43,7 +43,7 @@ const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( [labelId], ); - const contextValue: SelectOptionGroupContext = React.useMemo( + const contextValue: SelectGroupContext = React.useMemo( () => ({ labelId, setLabelId, @@ -61,20 +61,20 @@ const SelectOptionGroup = React.forwardRef(function SelectOptionGroup( }); return ( - + {renderElement()} - + ); }); -namespace SelectOptionGroup { +namespace SelectGroup { export interface OwnerState { open: boolean; } export interface Props extends BaseUIComponentProps<'div', OwnerState> {} } -SelectOptionGroup.propTypes /* remove-proptypes */ = { +SelectGroup.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -93,4 +93,4 @@ SelectOptionGroup.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -export { SelectOptionGroup }; +export { SelectGroup }; diff --git a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroupContext.ts b/packages/mui-base/src/Select/Group/SelectGroupContext.ts similarity index 52% rename from packages/mui-base/src/Select/OptionGroup/SelectOptionGroupContext.ts rename to packages/mui-base/src/Select/Group/SelectGroupContext.ts index 47b885bcf..1cd08b6f8 100644 --- a/packages/mui-base/src/Select/OptionGroup/SelectOptionGroupContext.ts +++ b/packages/mui-base/src/Select/Group/SelectGroupContext.ts @@ -1,14 +1,14 @@ import * as React from 'react'; -export interface SelectOptionGroupContext { +export interface SelectGroupContext { labelId: string | undefined; setLabelId: React.Dispatch>; } -export const SelectOptionGroupContext = React.createContext(null); +export const SelectGroupContext = React.createContext(null); -export function useSelectOptionGroupContext() { - const context = React.useContext(SelectOptionGroupContext); +export function useSelectGroupContext() { + const context = React.useContext(SelectGroupContext); if (context === null) { throw new Error('Base UI: must be used within a '); } diff --git a/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.test.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx similarity index 68% rename from packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.test.tsx rename to packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx index b3a1b5ad5..7e7c0c4a8 100644 --- a/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.test.tsx +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; -describe('', () => { +describe('', () => { const { render } = createRenderer(); - describeConformance(, () => ({ + describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, render(node) { return render( - {node} + {node} , ); }, diff --git a/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx similarity index 80% rename from packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx rename to packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx index 1ce12afa1..ff5902f23 100644 --- a/packages/mui-base/src/Select/OptionGroupLabel/SelectOptionGroupLabel.tsx +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx @@ -5,9 +5,10 @@ import type { BaseUIComponentProps } from '../../utils/types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useId } from '../../utils/useId'; -import { useSelectOptionGroupContext } from '../OptionGroup/SelectOptionGroupContext'; +import { useSelectGroupContext } from '../Group/SelectGroupContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useSelectRootContext } from '../Root/SelectRootContext'; + /** * * Demos: @@ -16,18 +17,18 @@ import { useSelectRootContext } from '../Root/SelectRootContext'; * * API: * - * - [SelectOptionGroupLabel API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectOptionGroupLabel) + * - [SelectGroupLabel API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectGroupLabel) */ -const SelectOptionGroupLabel = React.forwardRef(function SelectOptionGroupLabel( - props: SelectOptionGroupLabel.Props, +const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( + props: SelectGroupLabel.Props, forwardedRef: React.ForwardedRef, ) { const { className, render, id: idProp, ...otherProps } = props; const { open } = useSelectRootContext(); - const { setLabelId } = useSelectOptionGroupContext(); + const { setLabelId } = useSelectGroupContext(); - const ownerState: SelectOptionGroupLabel.OwnerState = React.useMemo( + const ownerState: SelectGroupLabel.OwnerState = React.useMemo( () => ({ open, }), @@ -60,7 +61,7 @@ const SelectOptionGroupLabel = React.forwardRef(function SelectOptionGroupLabel( return renderElement(); }); -SelectOptionGroupLabel.propTypes /* remove-proptypes */ = { +SelectGroupLabel.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -83,11 +84,11 @@ SelectOptionGroupLabel.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -namespace SelectOptionGroupLabel { +namespace SelectGroupLabel { export interface OwnerState { open: boolean; } export interface Props extends BaseUIComponentProps<'div', OwnerState> {} } -export { SelectOptionGroupLabel }; +export { SelectGroupLabel }; diff --git a/packages/mui-base/src/Select/Option/SelectOption.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx index 4fdb4d156..09fc60803 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -179,7 +179,7 @@ namespace SelectOption { /** * The value of the select option. */ - value: string; + value?: unknown; /** * The click handler for the select option. */ @@ -227,7 +227,7 @@ SelectOption.propTypes /* remove-proptypes */ = { /** * The value of the select option. */ - value: PropTypes.string.isRequired, + value: PropTypes.any, } as any; export { SelectOption }; diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.tsx index 3bc3b304d..1b6e8128f 100644 --- a/packages/mui-base/src/Select/Popup/SelectPopup.tsx +++ b/packages/mui-base/src/Select/Popup/SelectPopup.tsx @@ -20,6 +20,7 @@ const customStyleHookMapping: CustomStyleHookMapping = { return value ? { 'data-select-exiting': '' } : null; }, }; + /** * * Demos: diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 2197a0b79..9cd993cdb 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import type { GenericHTMLProps } from '../../utils/types'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useSelectRootContext } from '../Root/SelectRootContext'; +import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; +import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; /** * @@ -17,26 +19,48 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { touchModality, } = useSelectRootContext(); + const { isPositioned } = useSelectPositionerContext(); + const hasSelectedIndex = selectedIndex !== null; + const [pointerEvents, setPointerEvents] = React.useState<'auto' | 'none'>('none'); + const getPopupProps: useSelectPopup.ReturnValue['getPopupProps'] = React.useCallback( (externalProps = {}) => { return mergeReactProps<'div'>(getRootPopupProps(externalProps), { style: { - // must be relative to the element. - position: 'relative', overflowY: 'auto', + ...(pointerEvents === 'none' && { pointerEvents }), ...(alignMethod && hasSelectedIndex && !touchModality && { + // Note: not supported in Safari. Needs to be manually specified in CSS. scrollbarWidth: 'none', }), }, }); }, - [getRootPopupProps, alignMethod, hasSelectedIndex, touchModality], + [getRootPopupProps, pointerEvents, alignMethod, hasSelectedIndex, touchModality], ); + useEnhancedEffect(() => { + if (!isPositioned) { + return undefined; + } + + // `isPositioned` becomes `true` for the _initial_ positioning, excluding fallbacks. We need + // to wait for any fallbacks to have been potentially applied though. This also appears to fix + // a bug in Safari where focus gets lost when the item is opened on top of the trigger. + const frame = requestAnimationFrame(() => { + setPointerEvents('auto'); + }); + + return () => { + cancelAnimationFrame(frame); + setPointerEvents('none'); + }; + }, [isPositioned]); + return React.useMemo( () => ({ getPopupProps, diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index e4094c353..8483f3fa3 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -208,10 +208,15 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const positionerElement = renderElement(); - const stringValue = React.useMemo( - () => (typeof value === 'string' ? value : JSON.stringify(value)), - [value], - ); + const serializedValue = React.useMemo(() => { + if (value == null) { + return ''; // avoid uncontrolled -> controlled error + } + if (typeof value === 'string') { + return value; + } + return JSON.stringify(value); + }, [value]); const mountedItemsElement = keepMounted ? null : ; const nativeSelectElement = ( @@ -243,7 +248,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( }, })} > - + ); diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index 65633b150..21c50374f 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -5,8 +5,8 @@ export { SelectPopup } from './Popup/SelectPopup'; export { SelectBackdrop } from './Backdrop/SelectBackdrop'; export { SelectOption } from './Option/SelectOption'; export { SelectOptionIndicator } from './OptionIndicator/SelectOptionIndicator'; -export { SelectOptionGroup } from './OptionGroup/SelectOptionGroup'; -export { SelectOptionGroupLabel } from './OptionGroupLabel/SelectOptionGroupLabel'; +export { SelectGroup } from './Group/SelectGroup'; +export { SelectGroupLabel } from './GroupLabel/SelectGroupLabel'; export { SelectValue } from './Value/SelectValue'; export { SelectScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; export { SelectScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index ef3075a5e..ed212ac3b 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -5,8 +5,8 @@ export { SelectPopup as Popup } from './Popup/SelectPopup'; export { SelectBackdrop as Backdrop } from './Backdrop/SelectBackdrop'; export { SelectOption as Option } from './Option/SelectOption'; export { SelectOptionIndicator as OptionIndicator } from './OptionIndicator/SelectOptionIndicator'; -export { SelectOptionGroup as OptionGroup } from './OptionGroup/SelectOptionGroup'; -export { SelectOptionGroupLabel as OptionGroupLabel } from './OptionGroupLabel/SelectOptionGroupLabel'; +export { SelectGroup as Group } from './Group/SelectGroup'; +export { SelectGroupLabel as GroupLabel } from './GroupLabel/SelectGroupLabel'; export { SelectValue as Value } from './Value/SelectValue'; export { SelectScrollUpArrow as ScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; From 729144e864d3e983a6aeb065618a55825fa2dce4 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 11 Sep 2024 12:44:55 +1000 Subject: [PATCH 54/94] Update APIs --- docs/data/components/select/SelectEmpty.js | 70 +++++--- docs/data/components/select/SelectEmpty.tsx | 70 +++++--- docs/data/components/select/SelectGroup.js | 66 ++++++-- docs/data/components/select/SelectGroup.tsx | 66 ++++++-- .../select/SelectIntroduction/system/index.js | 70 +++++--- .../SelectIntroduction/system/index.tsx | 70 +++++--- docs/data/components/select/SelectSimple.js | 158 ------------------ .../src/Select/Trigger/SelectTrigger.tsx | 2 +- 8 files changed, 297 insertions(+), 275 deletions(-) delete mode 100644 docs/data/components/select/SelectSimple.js diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js index 7fa7db27f..725c02125 100644 --- a/docs/data/components/select/SelectEmpty.js +++ b/docs/data/components/select/SelectEmpty.js @@ -3,8 +3,6 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; export default function SelectEmpty() { return ( @@ -15,39 +13,64 @@ export default function SelectEmpty() { - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + /> - } /> + } /> Select font... - } /> + } /> System font - } /> + } /> Arial - } /> + } /> Roboto - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + />
            ); } +const CheckIcon = styled(function CheckIcon(props) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; @@ -124,6 +147,8 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` const scrollArrowStyles = css` width: 100%; height: 15px; + font-size: 10px; + cursor: default; &[data-side='none'] { height: 25px; @@ -138,25 +163,30 @@ const scrollArrowStyles = css` align-items: center; justify-content: center; border-radius: 5px; - top: 0; } `; const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; ${scrollArrowStyles} &[data-side='none'] { top: -10px; + + > div { + top: 10px; + } } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; ${scrollArrowStyles} + bottom: 0; &[data-side='none'] { bottom: -10px; + + > div { + bottom: 10px; + } } `; diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx index 7fa7db27f..60300b21b 100644 --- a/docs/data/components/select/SelectEmpty.tsx +++ b/docs/data/components/select/SelectEmpty.tsx @@ -3,8 +3,6 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; export default function SelectEmpty() { return ( @@ -15,39 +13,64 @@ export default function SelectEmpty() { - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + /> - } /> + } /> Select font... - } /> + } /> System font - } /> + } /> Arial - } /> + } /> Roboto - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + />
            ); } +const CheckIcon = styled(function CheckIcon(props: React.SVGProps) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; @@ -124,6 +147,8 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` const scrollArrowStyles = css` width: 100%; height: 15px; + font-size: 10px; + cursor: default; &[data-side='none'] { height: 25px; @@ -138,25 +163,30 @@ const scrollArrowStyles = css` align-items: center; justify-content: center; border-radius: 5px; - top: 0; } `; const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; ${scrollArrowStyles} &[data-side='none'] { top: -10px; + + > div { + top: 10px; + } } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; ${scrollArrowStyles} + bottom: 0; &[data-side='none'] { bottom: -10px; + + > div { + bottom: 10px; + } } `; diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js index d560efca7..f1c22bbd6 100644 --- a/docs/data/components/select/SelectGroup.js +++ b/docs/data/components/select/SelectGroup.js @@ -3,8 +3,6 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; function createOptions(items) { return items.map((item) => ({ @@ -36,14 +34,16 @@ export default function SelectGroup() { - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + /> - } /> + } /> Select food... {entries.map(([group, items]) => ( @@ -57,7 +57,7 @@ export default function SelectGroup() { value={item.value} disabled={item.value === 'banana'} > - } /> + } /> {item.label} ))} @@ -65,16 +65,39 @@ export default function SelectGroup() { ))} - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + />
            ); } +const CheckIcon = styled(function CheckIcon(props) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + const gray = { 300: '#e5e7eb', }; @@ -163,6 +186,8 @@ const SelectGroupLabel = styled(Select.GroupLabel)` const scrollArrowStyles = css` width: 100%; height: 15px; + font-size: 10px; + cursor: default; &[data-side='none'] { height: 25px; @@ -177,26 +202,31 @@ const scrollArrowStyles = css` align-items: center; justify-content: center; border-radius: 5px; - top: 0; } `; const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; ${scrollArrowStyles} &[data-side='none'] { top: -10px; + + > div { + top: 10px; + } } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; ${scrollArrowStyles} + bottom: 0; &[data-side='none'] { bottom: -10px; + + > div { + bottom: 10px; + } } `; diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx index 1d9b3c6ef..e5e43f88e 100644 --- a/docs/data/components/select/SelectGroup.tsx +++ b/docs/data/components/select/SelectGroup.tsx @@ -3,8 +3,6 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; function createOptions(items: string[]) { return items.map((item) => ({ @@ -36,14 +34,16 @@ export default function SelectGroup() { - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + /> - } /> + } /> Select food... {entries.map(([group, items]) => ( @@ -57,7 +57,7 @@ export default function SelectGroup() { value={item.value} disabled={item.value === 'banana'} > - } /> + } /> {item.label} ))} @@ -65,16 +65,39 @@ export default function SelectGroup() { ))} - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + />
            ); } +const CheckIcon = styled(function CheckIcon(props: React.SVGProps) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + const gray = { 300: '#e5e7eb', }; @@ -163,6 +186,8 @@ const SelectGroupLabel = styled(Select.GroupLabel)` const scrollArrowStyles = css` width: 100%; height: 15px; + font-size: 10px; + cursor: default; &[data-side='none'] { height: 25px; @@ -177,26 +202,31 @@ const scrollArrowStyles = css` align-items: center; justify-content: center; border-radius: 5px; - top: 0; } `; const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; ${scrollArrowStyles} &[data-side='none'] { top: -10px; + + > div { + top: 10px; + } } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; ${scrollArrowStyles} + bottom: 0; &[data-side='none'] { bottom: -10px; + + > div { + bottom: 10px; + } } `; diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 41c889358..1728fb0e1 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -3,47 +3,70 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; export default function SelectSimple() { return ( - + - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + /> - } /> + } /> System font - } /> + } /> Arial - } /> + } /> Roboto - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + />
            ); } +const CheckIcon = styled(function CheckIcon(props) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; @@ -120,6 +143,8 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` const scrollArrowStyles = css` width: 100%; height: 15px; + font-size: 10px; + cursor: default; &[data-side='none'] { height: 25px; @@ -134,25 +159,30 @@ const scrollArrowStyles = css` align-items: center; justify-content: center; border-radius: 5px; - top: 0; } `; const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; ${scrollArrowStyles} &[data-side='none'] { top: -10px; + + > div { + top: 10px; + } } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; ${scrollArrowStyles} + bottom: 0; &[data-side='none'] { bottom: -10px; + + > div { + bottom: 10px; + } } `; diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 41c889358..57663e232 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -3,47 +3,70 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; export default function SelectSimple() { return ( - + - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + /> - } /> + } /> System font - } /> + } /> Arial - } /> + } /> Roboto - -
            - -
            -
            + ( +
            +
            {props.children}
            +
            + )} + />
            ); } +const CheckIcon = styled(function CheckIcon(props: React.SVGProps) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; @@ -120,6 +143,8 @@ const SelectOptionIndicator = styled(Select.OptionIndicator)` const scrollArrowStyles = css` width: 100%; height: 15px; + font-size: 10px; + cursor: default; &[data-side='none'] { height: 25px; @@ -134,25 +159,30 @@ const scrollArrowStyles = css` align-items: center; justify-content: center; border-radius: 5px; - top: 0; } `; const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; ${scrollArrowStyles} &[data-side='none'] { top: -10px; + + > div { + top: 10px; + } } `; const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; ${scrollArrowStyles} + bottom: 0; &[data-side='none'] { bottom: -10px; + + > div { + bottom: 10px; + } } `; diff --git a/docs/data/components/select/SelectSimple.js b/docs/data/components/select/SelectSimple.js deleted file mode 100644 index 41c889358..000000000 --- a/docs/data/components/select/SelectSimple.js +++ /dev/null @@ -1,158 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; -import { css, styled } from '@mui/system'; -import Check from '@mui/icons-material/Check'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; - -export default function SelectSimple() { - return ( - - - - - - - - -
            - -
            -
            - - - } /> - System font - - - } /> - Arial - - - } /> - Roboto - - - -
            - -
            -
            -
            -
            - ); -} - -const SelectTrigger = styled(Select.Trigger)` - font-family: 'IBM Plex Sans', sans-serif; - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 12px; - border-radius: 5px; - background-color: black; - color: white; - border: none; - font-size: 100%; - line-height: 1.5; - user-select: none; - cursor: default; - - &:focus-visible { - outline: 2px solid black; - outline-offset: 2px; - } -`; - -const SelectDropdownArrow = styled(Select.Icon)` - margin-left: 6px; - font-size: 10px; - line-height: 1; - height: 6px; -`; - -const SelectPopup = styled(Select.Popup)` - background-color: white; - padding: 4px; - border-radius: 5px; - box-shadow: - 0 2px 4px rgb(0 0 0 / 0.1), - 0 0 0 1px rgb(0 0 0 / 0.1); - max-height: var(--available-height); - outline: 0; - min-width: calc(var(--anchor-width) + 20px); -`; - -const SelectOption = styled(Select.Option)` - padding: 6px 16px 6px 4px; - outline: 0; - cursor: default; - border-radius: 4px; - user-select: none; - display: flex; - align-items: center; - line-height: 1.5; - scroll-margin: 15px; - - &[data-disabled] { - opacity: 0.5; - } - - &[data-highlighted], - &:focus { - background-color: black; - color: white; - } -`; - -const SelectOptionIndicator = styled(Select.OptionIndicator)` - margin-right: 4px; - visibility: hidden; - width: 16px; - height: 16px; - - &[data-selected] { - visibility: visible; - } -`; - -const scrollArrowStyles = css` - width: 100%; - height: 15px; - - &[data-side='none'] { - height: 25px; - } - - > div { - position: absolute; - background: white; - width: 100%; - height: 15px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 5px; - top: 0; - } -`; - -const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` - transform: rotate(180deg); - top: 0; - ${scrollArrowStyles} - - &[data-side='none'] { - top: -10px; - } -`; - -const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` - bottom: 0; - ${scrollArrowStyles} - - &[data-side='none'] { - bottom: -10px; - } -`; diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx index 4df059a5f..9ca78321c 100644 --- a/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx +++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.tsx @@ -7,7 +7,7 @@ import { commonStyleHooks } from '../utils/commonStyleHooks'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import { BaseUIComponentProps } from '../../utils/types'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; -import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; + /** * * Demos: From 466cf5f2ab5afd6cbd6afaba0e796a9c50a934e5 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 11 Sep 2024 13:45:39 +1000 Subject: [PATCH 55/94] Fix test --- packages/mui-base/src/Field/Root/FieldRoot.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index cc4255436..7204b25bb 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -24,10 +24,10 @@ describe('', () => { })); describe('prop: disabled', () => { - it('should add data-disabled style hook to all components', () => { - render( + it('should add data-disabled style hook to all components', async () => { + await render( - + , From 4b52e468d7aedc505a3f19a87311af81a2cd7482 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 11 Sep 2024 14:07:55 +1000 Subject: [PATCH 56/94] Fix null default --- docs/data/api/select-option.json | 2 +- docs/data/components/select/SelectGroup.js | 2 +- docs/data/components/select/SelectGroup.tsx | 2 +- .../select/SelectIntroduction/system/index.js | 2 +- .../SelectIntroduction/system/index.tsx | 2 +- .../src/Select/Option/SelectOption.tsx | 8 ++++--- .../Select/Positioner/SelectPositioner.tsx | 2 +- .../src/Select/Root/SelectRoot.test.tsx | 2 +- .../mui-base/src/Select/Root/SelectRoot.tsx | 6 ++--- .../src/Select/Root/useSelectRoot.tsx | 23 ++++++++----------- .../mui-base/src/Select/Value/SelectValue.tsx | 2 +- 11 files changed, 25 insertions(+), 28 deletions(-) diff --git a/docs/data/api/select-option.json b/docs/data/api/select-option.json index dd2f64a59..7bc4547e8 100644 --- a/docs/data/api/select-option.json +++ b/docs/data/api/select-option.json @@ -3,7 +3,7 @@ "disabled": { "type": { "name": "bool" }, "default": "false" }, "label": { "type": { "name": "string" } }, "onClick": { "type": { "name": "func" } }, - "value": { "type": { "name": "any" } } + "value": { "type": { "name": "any" }, "default": "null" } }, "name": "SelectOption", "imports": [ diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js index f1c22bbd6..4fd7a325e 100644 --- a/docs/data/components/select/SelectGroup.js +++ b/docs/data/components/select/SelectGroup.js @@ -42,7 +42,7 @@ export default function SelectGroup() { )} /> - + } /> Select food... diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx index e5e43f88e..fb5261359 100644 --- a/docs/data/components/select/SelectGroup.tsx +++ b/docs/data/components/select/SelectGroup.tsx @@ -42,7 +42,7 @@ export default function SelectGroup() { )} /> - + } /> Select food... diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 1728fb0e1..067df8829 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -6,7 +6,7 @@ import { css, styled } from '@mui/system'; export default function SelectSimple() { return ( - + diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 57663e232..e0ec33a92 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -6,7 +6,7 @@ import { css, styled } from '@mui/system'; export default function SelectSimple() { return ( - + diff --git a/packages/mui-base/src/Select/Option/SelectOption.tsx b/packages/mui-base/src/Select/Option/SelectOption.tsx index 09fc60803..91127a945 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.tsx @@ -88,7 +88,7 @@ const SelectOption = React.forwardRef(function SelectOption( props: SelectOption.Props, forwardedRef: React.ForwardedRef, ) { - const { id: idProp, value: valueProp, label, ...otherProps } = props; + const { id: idProp, value: valueProp = null, label, ...otherProps } = props; const { setValue, @@ -114,7 +114,7 @@ const SelectOption = React.forwardRef(function SelectOption( values[listItem.index] = valueProp; return () => { - values[listItem.index] = null; + delete values[listItem.index]; }; }, [listItem.index, valueProp, valuesRef]); @@ -178,8 +178,9 @@ namespace SelectOption { children?: React.ReactNode; /** * The value of the select option. + * @default null */ - value?: unknown; + value?: any; /** * The click handler for the select option. */ @@ -226,6 +227,7 @@ SelectOption.propTypes /* remove-proptypes */ = { onClick: PropTypes.func, /** * The value of the select option. + * @default null */ value: PropTypes.any, } as any; diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 8483f3fa3..6fe1175de 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -226,7 +226,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( name, disabled, required, - value, + value: serializedValue, ref: inputRef, style: visuallyHidden, tabIndex: -1, diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index 4faeda345..53efa319b 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -122,7 +122,7 @@ describe('', () => { const handleValueChange = spy(); function App() { - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState(''); return ( void; + onValueChange?: (value: any, event?: Event) => void; /** * The default value of the select. */ - defaultValue?: unknown; + defaultValue?: any; /** * If `true`, the Select is initially open. * diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 7ce7bbd05..43cba94a3 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -39,7 +39,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R loop, value: valueProp, onValueChange, - defaultValue = '', + defaultValue = null, alignMethod, } = params; @@ -60,7 +60,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const typingRef = React.useRef(false); const elementsRef = React.useRef>([]); const labelsRef = React.useRef>([]); - const valuesRef = React.useRef>([]); + const valuesRef = React.useRef>([]); const selectionRef = React.useRef({ allowMouseUp: false, allowSelect: false }); const overflowRef = React.useRef({ top: 0, bottom: 0, left: 0, right: 0 }); const valueRef = React.useRef(null); @@ -89,14 +89,9 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R fieldControlValidation.commitValidation(nextValue); } - if (nextValue !== null) { - const index = valuesRef.current.indexOf(nextValue); - setSelectedIndex(index); - setLabel(labelsRef.current[index] ?? ''); - } else { - setSelectedIndex(null); - setLabel(''); - } + const index = valuesRef.current.indexOf(nextValue); + setSelectedIndex(index); + setLabel(labelsRef.current[index] ?? ''); }); useEnhancedEffect(() => { @@ -304,16 +299,16 @@ export namespace useSelectRoot { /** * The value of the Select. Use when controlled. */ - value?: unknown; + value?: any; /** * Callback fired when the value of the Select changes. Use when controlled. */ - onValueChange?: (value: unknown, event?: Event) => void; + onValueChange?: (value: any, event?: Event) => void; /** * The default value of the Select. * @default '' */ - defaultValue?: unknown; + defaultValue?: any; /** * Determines if the select should align to the selected item inside the popup or the trigger * element. @@ -337,7 +332,7 @@ export namespace useSelectRoot { getTriggerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; elementsRef: React.MutableRefObject<(HTMLElement | null)[]>; labelsRef: React.MutableRefObject>; - valuesRef: React.MutableRefObject>; + valuesRef: React.MutableRefObject>; mounted: boolean; open: boolean; popupRef: React.RefObject; diff --git a/packages/mui-base/src/Select/Value/SelectValue.tsx b/packages/mui-base/src/Select/Value/SelectValue.tsx index 8be0e103c..f4bfccfe3 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.tsx @@ -72,7 +72,7 @@ SelectValue.propTypes /* remove-proptypes */ = { PropTypes.func, PropTypes.number, PropTypes.shape({ - '__@iterator@70': PropTypes.func.isRequired, + '__@iterator@74': PropTypes.func.isRequired, }), PropTypes.shape({ children: PropTypes.node, From 3bf30e22273a69f514b27ee5f40488200b56e9bf Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 11 Sep 2024 19:16:22 +1000 Subject: [PATCH 57/94] Docs, fixes --- docs/data/components/select/SelectEmpty.js | 1 + docs/data/components/select/SelectEmpty.tsx | 1 + docs/data/components/select/select.mdx | 184 +++++++++++++++++- .../Select/OptionText/SelectOptionText.tsx | 5 +- .../src/Select/Popup/useSelectPopup.ts | 2 +- .../src/Select/Root/useSelectRoot.tsx | 5 + packages/mui-base/src/utils/warn.ts | 14 ++ 7 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 packages/mui-base/src/utils/warn.ts diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js index 725c02125..0a85d6b49 100644 --- a/docs/data/components/select/SelectEmpty.js +++ b/docs/data/components/select/SelectEmpty.js @@ -109,6 +109,7 @@ const SelectPopup = styled(Select.Popup)` max-height: var(--available-height); outline: 0; min-width: calc(var(--anchor-width) + 20px); + max-width: var(--available-width); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx index 60300b21b..697eb1b21 100644 --- a/docs/data/components/select/SelectEmpty.tsx +++ b/docs/data/components/select/SelectEmpty.tsx @@ -109,6 +109,7 @@ const SelectPopup = styled(Select.Popup)` max-height: var(--available-height); outline: 0; min-width: calc(var(--anchor-width) + 20px); + max-width: var(--available-width); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index 8e7d55a5d..74794c4c0 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -82,7 +82,7 @@ To set an initial value when uncontrolled, use `defaultValue`: ## Empty value -The select's value is empty (`""`) by default, which enables an empty state: +The select's value is empty (`null`) by default, which enables an empty `Option` to be initially selected when it has no `value` prop: @@ -147,9 +147,25 @@ Two different methods to align the popup are available: ``` -- **`item`**: aligns the popup such that the selected item inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. This method is useful as it allows the user to select the an item in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS. The scroll arrow components must be used to ensure this is the case. +- **`item`**: aligns the popup such that the selected option inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. This method is useful as it allows the user to select the an option in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS. The scroll arrow components must be used to ensure this is the case. - **`trigger`**: aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. +### Scrollable popup + +When using `alignMethod="trigger"`, the select's height needs to be manually limited by its available space using CSS. + +This can be achieved by using the `--available-height` CSS variable: + +```jsx + +``` + +```css +.SelectPopup { + max-height: var(--available-height); +} +``` + ## Value component The `Select.Value` subcomponent renders the selected value. This is the text content or `label` of `Select.Option` by default. @@ -167,3 +183,167 @@ A function can be specified as a child to customize the rendering of the value: ```jsx {(value) => value.toLowerCase()} ``` + +## Arrow + +To add an arrow (caret or triangle) inside the select popup that points toward the center of the anchor element, use the `Select.Arrow` component: + +Note: this is only supported when using `alignMethod="trigger"`. + +```jsx + + + + +``` + +It automatically positions a wrapper element that can be styled or contain a custom SVG shape. + +## Animations + +The select can animate when opening or closing with either: + +- CSS transitions +- CSS animations +- JavaScript animations + +### CSS transitions + +Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior: + +```jsx + + Option 1 + +``` + +```css +.SelectPopup { + transform-origin: var(--transform-origin); + transition-property: opacity, transform; + transition-duration: 0.2s; + /* Represents the final styles once exited */ + opacity: 0; + transform: scale(0.9); +} + +/* Represents the final styles once entered */ +.SelectPopup[data-select='open'] { + opacity: 1; + transform: scale(1); +} + +/* Represents the initial styles when entering */ +.SelectPopup[data-entering] { + opacity: 0; + transform: scale(0.9); +} +``` + +Styles need to be applied in three states: + +- The exiting styles, placed on the base element class +- The open styles, placed on the base element class with `[data-state="open"]` +- The entering styles, placed on the base element class with `[data-entering]` + +In newer browsers, there is a feature called `@starting-style` which allows transitions to occur on open for conditionally-mounted components: + +```css +/* Base UI API - Polyfill */ +.SelectPopup[data-entering] { + opacity: 0; + transform: scale(0.9); +} + +/* Official Browser API - no Firefox support as of May 2024 */ +@starting-style { + .SelectPopup[data-select='open'] { + opacity: 0; + transform: scale(0.9); + } +} +``` + +### CSS animations + +CSS animations can also be used, requiring only two separate declarations: + +```css +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.9); + } +} + +@keyframes scale-out { + to { + opacity: 0; + transform: scale(0.9); + } +} + +.SelectPopup { + animation: scale-in 0.2s forwards; +} + +.SelectPopup[data-exiting] { + animation: scale-out 0.2s forwards; +} +``` + +### JavaScript animations + +The `keepMounted` prop lets an external library control the mounting, for example `framer-motion`'s `AnimatePresence` component. + +```js +function App() { + const [open, setOpen] = useState(false); + return ( + + Trigger + + {open && ( + + + } + > + Option 1 + Option 2 + + + )} + + + ); +} +``` + +### Animation states + +Four states are available as data attributes to animate the popup, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the `keepMounted` prop. + +- `[data-select="open"]` - `open` state is `true`. +- `[data-select="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing. +- `[data-entering]` - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering. +- `[data-exiting]` - the popup is in the process of being removed from the DOM, but is still mounted. + +## Overriding default components + +Use the `render` prop to override the rendered elements with your own components. + +```jsx +// Element shorthand +} /> +``` + +```jsx +// Function + } /> +``` diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index dba6c8518..db0ed4ef0 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -24,7 +24,8 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( ) { const { className, render, ...otherProps } = props; - const { open, triggerElement, valueRef, popupRef, innerFallback } = useSelectRootContext(); + const { open, triggerElement, valueRef, popupRef, innerFallback, alignMethod } = + useSelectRootContext(); const { isPositioned, setOptionTextOffset } = useSelectPositionerContext(); const { selected } = useSelectOptionContext(); @@ -35,6 +36,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( useEnhancedEffect(() => { if ( + alignMethod !== 'item' || innerFallback || !open || !isPositioned || @@ -57,6 +59,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( setOptionTextOffset(triggerDiff - popupDiff); }, [ + alignMethod, innerFallback, open, isPositioned, diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 9cd993cdb..2da7a929c 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -31,7 +31,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { style: { overflowY: 'auto', ...(pointerEvents === 'none' && { pointerEvents }), - ...(alignMethod && + ...(alignMethod === 'item' && hasSelectedIndex && !touchModality && { // Note: not supported in Safari. Needs to be manually specified in CSS. diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 43cba94a3..b18448403 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -22,6 +22,7 @@ import { useControlled } from '../../utils/useControlled'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; +import { warn } from '../../utils/warn'; /** * @@ -101,6 +102,10 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R if (index !== -1) { setSelectedIndex(index); setLabel(labelsRef.current[index] ?? ''); + } else if (value) { + warn( + `The value \`${typeof value === 'string' ? value : JSON.stringify(value)}\` is not present in the Select options.`, + ); } }); }, [value]); diff --git a/packages/mui-base/src/utils/warn.ts b/packages/mui-base/src/utils/warn.ts new file mode 100644 index 000000000..9a5382870 --- /dev/null +++ b/packages/mui-base/src/utils/warn.ts @@ -0,0 +1,14 @@ +let set: Set; +if (process.env.NODE_ENV !== 'production') { + set = new Set(); +} + +export function warn(...messages: string[]) { + if (process.env.NODE_ENV !== 'production') { + const messageKey = messages.join(' '); + if (!set.has(messageKey)) { + set.add(messageKey); + console.warn(`Base UI: ${messageKey}`); + } + } +} From f2d8fa86e7b49bd7b2fcfba92495a3ce523d39ad Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 11 Sep 2024 19:27:54 +1000 Subject: [PATCH 58/94] Fix prop-type --- .../mui-base/src/Select/Value/SelectValue.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/mui-base/src/Select/Value/SelectValue.tsx b/packages/mui-base/src/Select/Value/SelectValue.tsx index f4bfccfe3..06512e363 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.tsx @@ -51,7 +51,7 @@ const SelectValue = React.forwardRef(function SelectValue( namespace SelectValue { export interface OwnerState {} export interface Props extends Omit, 'children'> { - children?: React.ReactNode | ((value: string) => React.ReactNode); + children?: null | ((value: string) => React.ReactNode); /** * The placeholder value to display when the value is empty (such as during SSR). */ @@ -67,22 +67,7 @@ SelectValue.propTypes /* remove-proptypes */ = { /** * @ignore */ - children: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.func, - PropTypes.number, - PropTypes.shape({ - '__@iterator@74': PropTypes.func.isRequired, - }), - PropTypes.shape({ - children: PropTypes.node, - key: PropTypes.string, - props: PropTypes.any.isRequired, - type: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired, - }), - PropTypes.string, - PropTypes.bool, - ]), + children: PropTypes.func, /** * Class names applied to the element or a function that returns them based on the component's state. */ From 4bacbbabc79667d12d97155dcefcbe4d8d272558 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 11 Sep 2024 19:37:57 +1000 Subject: [PATCH 59/94] Update anatomy --- docs/data/components/select/SelectEmpty.js | 1 - docs/data/components/select/SelectEmpty.tsx | 1 - docs/data/components/select/SelectGroup.js | 1 - docs/data/components/select/SelectGroup.tsx | 1 - .../select/SelectIntroduction/system/index.js | 1 - .../select/SelectIntroduction/system/index.tsx | 1 - docs/data/components/select/select.mdx | 14 +++++++------- 7 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js index 0a85d6b49..34db84ab9 100644 --- a/docs/data/components/select/SelectEmpty.js +++ b/docs/data/components/select/SelectEmpty.js @@ -11,7 +11,6 @@ export default function SelectEmpty() { - ( diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx index 697eb1b21..53aea67d6 100644 --- a/docs/data/components/select/SelectEmpty.tsx +++ b/docs/data/components/select/SelectEmpty.tsx @@ -11,7 +11,6 @@ export default function SelectEmpty() { - ( diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js index 4fd7a325e..7c2b9e056 100644 --- a/docs/data/components/select/SelectGroup.js +++ b/docs/data/components/select/SelectGroup.js @@ -32,7 +32,6 @@ export default function SelectGroup() { - ( diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx index fb5261359..a9117b43a 100644 --- a/docs/data/components/select/SelectGroup.tsx +++ b/docs/data/components/select/SelectGroup.tsx @@ -32,7 +32,6 @@ export default function SelectGroup() { - ( diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 067df8829..9d1bf5e21 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -11,7 +11,6 @@ export default function SelectSimple() { - ( diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index e0ec33a92..691a931ff 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -11,7 +11,6 @@ export default function SelectSimple() { - ( diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index 74794c4c0..bb3bc63c3 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -35,8 +35,8 @@ Selects are implemented using a collection of related components: - `` renders an option, placed inside the popup. - `` renders the text of an option. - `` renders an option indicator inside an option to indicate it's selected (e.g. a check icon). -- `` renders an option group, wrapping `` components. -- `` renders a label for an option group. +- `` renders an group for a set of options, wrapping `` components. +- `` renders a label for a group of options. - `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. - `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. - `` renders a separator between option groups. @@ -55,19 +55,19 @@ Selects are implemented using a collection of related components: - - + + - +
            - - + + ``` From d0d05f91bc23c66da9604e817031484f1524cad5 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 12 Sep 2024 14:44:16 +1000 Subject: [PATCH 60/94] Handle async innerOffset scroll arrow visibility change --- .../src/Select/ScrollArrow/SelectScrollArrow.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index 70d845435..fe88b2d23 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -141,11 +141,13 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( popupElement.addEventListener('wheel', handleScrollArrowVisible); popupElement.addEventListener('scroll', handleScrollArrowVisible); win.addEventListener('resize', handleScrollArrowVisible); + win.addEventListener('scroll', handleScrollArrowVisible); return () => { popupElement.removeEventListener('wheel', handleScrollArrowVisible); popupElement.removeEventListener('scroll', handleScrollArrowVisible); win.removeEventListener('resize', handleScrollArrowVisible); + win.removeEventListener('scroll', handleScrollArrowVisible); }; }, [inert, popupRef, direction, handleScrollArrowVisible]); @@ -155,7 +157,18 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( } handleScrollArrowVisible(); - }, [isPositioned, side, innerOffset, inert, handleScrollArrowVisible]); + }, [isPositioned, side, inert, handleScrollArrowVisible]); + + useEnhancedEffect(() => { + if (!isPositioned || inert) { + return; + } + + // Wait for the `innerOffset` to be applied in the DOM. While navigating with arrow keys, the + // scroll arrow might render even though it doesn't need to be visible because the select's + // height hasn't yet expanded. + requestAnimationFrame(handleScrollArrowVisible); + }, [isPositioned, inert, innerOffset, handleScrollArrowVisible]); const { renderElement } = useComponentRenderer({ propGetter: getScrollArrowProps, From 68194247403e82bf9acac415a9fc1c93e326a5c3 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 12 Sep 2024 14:57:24 +1000 Subject: [PATCH 61/94] Add waitFor for real browsers --- .../src/Select/Option/SelectOption.test.tsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/mui-base/src/Select/Option/SelectOption.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx index 63b342e5b..2c1f4a158 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.test.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; -import { fireEvent, flushMicrotasks, screen } from '@mui/internal-test-utils'; +import { fireEvent, flushMicrotasks, screen, waitFor } from '@mui/internal-test-utils'; import { createRenderer, describeConformance } from '#test-utils'; import { expect } from 'chai'; import userEvent from '@testing-library/user-event'; @@ -72,11 +72,15 @@ describe('', () => { await flushMicrotasks(); - expect(screen.getByText('one')).toHaveFocus(); + await waitFor(() => { + expect(screen.getByText('one')).toHaveFocus(); + }); await user.keyboard('{ArrowDown}'); - expect(screen.getByText('two')).toHaveFocus(); + await waitFor(() => { + expect(screen.getByText('two')).toHaveFocus(); + }); }); it('should select option when Enter key is pressed', async () => { @@ -104,7 +108,9 @@ describe('', () => { await user.keyboard('{ArrowDown}'); await user.keyboard('{Enter}'); - expect(value.textContent).to.equal('two'); + await waitFor(() => { + expect(value.textContent).to.equal('two'); + }); }); it('should not select disabled option', async () => { @@ -158,27 +164,22 @@ describe('', () => { await flushMicrotasks(); - expect(screen.getByRole('option', { name: 'one' })).toHaveFocus(); + await waitFor(() => { + expect(screen.getByRole('option', { name: 'one' })).toHaveFocus(); + }); await userEvent.keyboard('{ArrowDown}'); - - expect(screen.getByRole('option', { name: 'two' })).toHaveFocus(); - await userEvent.keyboard('{ArrowUp}'); - - expect(screen.getByRole('option', { name: 'one' })).toHaveFocus(); - await userEvent.keyboard('{ArrowUp}'); - expect(screen.getByRole('option', { name: 'three' })).toHaveFocus(); - fireEvent.click(screen.getByRole('option', { name: 'three' })); - fireEvent.click(trigger); await flushMicrotasks(); - expect(screen.getByRole('option', { name: 'three', hidden: false })).toHaveFocus(); + await waitFor(() => { + expect(screen.getByRole('option', { name: 'three', hidden: false })).toHaveFocus(); + }); }); describe('style hooks', () => { From e6b71126eb562ef4e9c63c54427eb2ef7ef8cd91 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 12 Sep 2024 15:13:16 +1000 Subject: [PATCH 62/94] Add placeholder in test --- packages/mui-base/src/Select/Option/SelectOption.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Option/SelectOption.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx index 2c1f4a158..a08609629 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.test.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.test.tsx @@ -87,7 +87,7 @@ describe('', () => { await render( - + From 3c0a89f439175f3288e6d64c866e997038a2177e Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 12 Sep 2024 15:13:31 +1000 Subject: [PATCH 63/94] Update SelectIntroduction component name --- docs/data/components/select/SelectIntroduction/system/index.js | 2 +- docs/data/components/select/SelectIntroduction/system/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 9d1bf5e21..0853b1be5 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -4,7 +4,7 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -export default function SelectSimple() { +export default function SelectIntroduction() { return ( diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 691a931ff..733688938 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import * as Select from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; -export default function SelectSimple() { +export default function SelectIntroduction() { return ( From 1874309ae571bbe904c98b3162e8847c7e3f1066 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 12 Sep 2024 15:25:32 +1000 Subject: [PATCH 64/94] Skip Enter test out of jsdom --- packages/mui-base/src/Select/Option/SelectOption.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Option/SelectOption.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx index a08609629..c61e24ba5 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.test.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.test.tsx @@ -83,7 +83,11 @@ describe('', () => { }); }); - it('should select option when Enter key is pressed', async () => { + it('should select option when Enter key is pressed', async function test() { + if (!/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + await render( From 2cdb698b095ec37df3b3864d110a2c643dbb4795 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 12 Sep 2024 18:16:56 +1000 Subject: [PATCH 65/94] Force fixed strategy when item-aligned --- .../mui-base/src/Select/Positioner/useSelectPositioner.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 698394b3d..1b9212c28 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -27,7 +27,9 @@ export function useSelectPositioner( const { touchModality, alignMethod, innerFallback, mounted } = useSelectRootContext(); - useScrollLock(alignMethod === 'item' && !innerFallback && mounted); + const itemAligned = alignMethod === 'item' && !innerFallback && !touchModality; + + useScrollLock(itemAligned && mounted); const { positionerStyles, @@ -41,11 +43,12 @@ export function useSelectPositioner( isPositioned, } = useAnchorPositioning({ ...params, + positionStrategy: itemAligned ? 'fixed' : params.positionStrategy, innerOptions: { fallback: params.innerFallback, touchModality, }, - trackAnchor: !(params.inner && !params.innerFallback), + trackAnchor: !itemAligned, collisionPadding: touchModality && params.collisionPadding == null ? 20 : params.collisionPadding, }); From fb65c175e59084c63c64036980763660fffdd626 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 17 Sep 2024 18:23:07 +1000 Subject: [PATCH 66/94] Integrate useField --- .../Select/Positioner/SelectPositioner.tsx | 20 ++++++++++++------- .../Select/Positioner/useSelectPositioner.tsx | 8 ++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 6fe1175de..28d33043f 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -17,6 +17,8 @@ import { useId } from '../../utils/useId'; import { useLatestRef } from '../../utils/useLatestRef'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { CompositeList } from '../../Composite/List/CompositeList'; +import { useField } from '../../Field/useField'; +import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; /** * Renders the element that positions the Select popup. @@ -78,7 +80,10 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( touchModality, } = useSelectRootContext(); - const { setControlId, validityData, setValidityData, setDirty } = useFieldRootContext(); + const { setControlId, validityData, setDirty } = useFieldRootContext(); + const { commitValidation } = useFieldControlValidation(); + + const triggerRef = useLatestRef(triggerElement); const id = useId(idProp); @@ -89,6 +94,13 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( }; }, [id, setControlId]); + useField({ + id, + commitValidation, + value, + controlRef: triggerRef, + }); + const [optionTextOffset, setOptionTextOffset] = React.useState(null); const [selectedIndexOnMount, setSelectedIndexOnMount] = React.useState(selectedIndex); @@ -104,12 +116,6 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( } }, [open, selectedIndexRef]); - useEnhancedEffect(() => { - if (validityData.initialValue === null && value !== validityData.initialValue) { - setValidityData((prev) => ({ ...prev, initialValue: value })); - } - }, [value, setValidityData, validityData.initialValue]); - const positioner = useSelectPositioner({ anchor: anchor || triggerElement, floatingRootContext, diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 1b9212c28..7a22bcec5 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -103,10 +103,6 @@ export function useSelectPositioner( export namespace useSelectPositioner { export interface SharedParameters { - /** - * If `true`, the Select is open. - */ - open?: boolean; /** * The anchor element to which the Select popup will be placed at. */ @@ -181,6 +177,10 @@ export namespace useSelectPositioner { } export interface Parameters extends SharedParameters { + /** + * If `true`, the Select is open. + */ + open?: boolean; /** * If `true`, the Select is mounted. * @default true From cb70b43cda4d5da1ed3cd77d2c2a98d5263c5e43 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 19 Sep 2024 14:59:06 +1000 Subject: [PATCH 67/94] Styles fixes --- docs/data/components/select/SelectEmpty.js | 14 ++++++++++---- docs/data/components/select/SelectEmpty.tsx | 14 ++++++++++---- docs/data/components/select/SelectGroup.js | 13 ++++++++++--- docs/data/components/select/SelectGroup.tsx | 13 ++++++++++--- .../select/SelectIntroduction/system/index.js | 13 ++++++++++--- .../select/SelectIntroduction/system/index.tsx | 13 ++++++++++--- .../mui-base/src/Select/Popup/useSelectPopup.ts | 1 - .../mui-base/src/utils/useAnchorPositioning.ts | 2 +- 8 files changed, 61 insertions(+), 22 deletions(-) diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js index 34db84ab9..cc3a018cf 100644 --- a/docs/data/components/select/SelectEmpty.js +++ b/docs/data/components/select/SelectEmpty.js @@ -70,12 +70,15 @@ const CheckIcon = styled(function CheckIcon(props) { height: 100%; `; +const triggerPaddingX = 6; +const popupPadding = 4; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; + padding: ${triggerPaddingX}px 12px; border-radius: 5px; background-color: black; color: white; @@ -99,16 +102,19 @@ const SelectDropdownArrow = styled(Select.Icon)` `; const SelectPopup = styled(Select.Popup)` + overflow-y: auto; background-color: white; - padding: 4px; + padding: ${popupPadding}px; border-radius: 5px; box-shadow: 0 2px 4px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 20px); - max-width: var(--available-width); + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx index 53aea67d6..a7c51747c 100644 --- a/docs/data/components/select/SelectEmpty.tsx +++ b/docs/data/components/select/SelectEmpty.tsx @@ -70,12 +70,15 @@ const CheckIcon = styled(function CheckIcon(props: React.SVGProps height: 100%; `; +const triggerPaddingX = 6; +const popupPadding = 4; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; + padding: ${triggerPaddingX}px 12px; border-radius: 5px; background-color: black; color: white; @@ -99,16 +102,19 @@ const SelectDropdownArrow = styled(Select.Icon)` `; const SelectPopup = styled(Select.Popup)` + overflow-y: auto; background-color: white; - padding: 4px; + padding: ${popupPadding}px; border-radius: 5px; box-shadow: 0 2px 4px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 20px); - max-width: var(--available-width); + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js index 7c2b9e056..d512baa00 100644 --- a/docs/data/components/select/SelectGroup.js +++ b/docs/data/components/select/SelectGroup.js @@ -101,12 +101,15 @@ const gray = { 300: '#e5e7eb', }; +const triggerPaddingX = 6; +const popupPadding = 4; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; + padding: ${triggerPaddingX}px 12px; border-radius: 5px; background-color: black; color: white; @@ -130,15 +133,19 @@ const SelectDropdownArrow = styled(Select.Icon)` `; const SelectPopup = styled(Select.Popup)` + overflow-y: auto; background-color: white; - padding: 4px; + padding: ${popupPadding}px; border-radius: 5px; box-shadow: 0 2px 4px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 20px); + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx index a9117b43a..405bfd129 100644 --- a/docs/data/components/select/SelectGroup.tsx +++ b/docs/data/components/select/SelectGroup.tsx @@ -101,12 +101,15 @@ const gray = { 300: '#e5e7eb', }; +const triggerPaddingX = 6; +const popupPadding = 4; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; + padding: ${triggerPaddingX}px 12px; border-radius: 5px; background-color: black; color: white; @@ -130,15 +133,19 @@ const SelectDropdownArrow = styled(Select.Icon)` `; const SelectPopup = styled(Select.Popup)` + overflow-y: auto; background-color: white; - padding: 4px; + padding: ${popupPadding}px; border-radius: 5px; box-shadow: 0 2px 4px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 20px); + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 0853b1be5..3b51d396d 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -66,12 +66,15 @@ const CheckIcon = styled(function CheckIcon(props) { height: 100%; `; +const triggerPaddingX = 6; +const popupPadding = 4; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; + padding: ${triggerPaddingX}px 12px; border-radius: 5px; background-color: black; color: white; @@ -95,15 +98,19 @@ const SelectDropdownArrow = styled(Select.Icon)` `; const SelectPopup = styled(Select.Popup)` + overflow-y: auto; background-color: white; - padding: 4px; + padding: ${popupPadding}px; border-radius: 5px; box-shadow: 0 2px 4px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 20px); + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); `; const SelectOption = styled(Select.Option)` diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 733688938..4710badad 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -66,12 +66,15 @@ const CheckIcon = styled(function CheckIcon(props: React.SVGProps height: 100%; `; +const triggerPaddingX = 6; +const popupPadding = 4; + const SelectTrigger = styled(Select.Trigger)` font-family: 'IBM Plex Sans', sans-serif; display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; + padding: ${triggerPaddingX}px 12px; border-radius: 5px; background-color: black; color: white; @@ -95,15 +98,19 @@ const SelectDropdownArrow = styled(Select.Icon)` `; const SelectPopup = styled(Select.Popup)` + overflow-y: auto; background-color: white; - padding: 4px; + padding: ${popupPadding}px; border-radius: 5px; box-shadow: 0 2px 4px rgb(0 0 0 / 0.1), 0 0 0 1px rgb(0 0 0 / 0.1); max-height: var(--available-height); outline: 0; - min-width: calc(var(--anchor-width) + 20px); + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); `; const SelectOption = styled(Select.Option)` diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 2da7a929c..62c2f0d85 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -29,7 +29,6 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { (externalProps = {}) => { return mergeReactProps<'div'>(getRootPopupProps(externalProps), { style: { - overflowY: 'auto', ...(pointerEvents === 'none' && { pointerEvents }), ...(alignMethod === 'item' && hasSelectedIndex && diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index 39ee87c08..c528db4f4 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -154,7 +154,7 @@ export function useAnchorPositioning( middleware.push( ...(!standardMode - ? [innerMiddleware] + ? [innerMiddleware, shiftMiddleware] : [ innerOptions.touchModality ? shift({ crossAxis: true, ...commonCollisionProps }) From 1a48ffd48b7292f181cb0e174013a49572ee1adf Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 19 Sep 2024 18:41:38 +1000 Subject: [PATCH 68/94] Export Select --- packages/mui-base/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts index c8ae7fc38..846c7bb3e 100644 --- a/packages/mui-base/src/index.ts +++ b/packages/mui-base/src/index.ts @@ -7,6 +7,7 @@ export * from './NumberField/index.barrel'; export * from './Popover/index.barrel'; export * from './Progress/index.barrel'; export * from './Separator/index.barrel'; +export * from './Select/index.barrel'; export * from './Slider/index.barrel'; export * from './Switch/index.barrel'; export * from './Tabs/index.barrel'; From dded295e3bcffa77196bec7c739add4830465ace Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 20 Sep 2024 18:06:54 +1000 Subject: [PATCH 69/94] Update packages/mui-base/src/Select/OptionText/SelectOptionText.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Dudak Signed-off-by: atomiks --- packages/mui-base/src/Select/OptionText/SelectOptionText.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index db0ed4ef0..19715ee39 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; From bec5433c0725b7ce3e762b9431e55da697a3f5be Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 20 Sep 2024 18:07:00 +1000 Subject: [PATCH 70/94] Update packages/mui-base/src/Select/Icon/SelectIcon.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Dudak Signed-off-by: atomiks --- packages/mui-base/src/Select/Icon/SelectIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Icon/SelectIcon.tsx b/packages/mui-base/src/Select/Icon/SelectIcon.tsx index be52fb120..aeb705fcb 100644 --- a/packages/mui-base/src/Select/Icon/SelectIcon.tsx +++ b/packages/mui-base/src/Select/Icon/SelectIcon.tsx @@ -43,7 +43,7 @@ const SelectIcon = React.forwardRef(function SelectIcon( namespace SelectIcon { export interface OwnerState {} - export interface Props extends BaseUIComponentProps<'div', OwnerState> {} + export interface Props extends BaseUIComponentProps<'span', OwnerState> {} } SelectIcon.propTypes /* remove-proptypes */ = { From 023897e9fc6f9113fc65366f2f184abcd930a523 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 20 Sep 2024 18:07:08 +1000 Subject: [PATCH 71/94] Update packages/mui-base/src/Select/Separator/SelectSeparator.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Dudak Signed-off-by: atomiks --- packages/mui-base/src/Select/Separator/SelectSeparator.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mui-base/src/Select/Separator/SelectSeparator.tsx b/packages/mui-base/src/Select/Separator/SelectSeparator.tsx index 962961f19..870e8cc09 100644 --- a/packages/mui-base/src/Select/Separator/SelectSeparator.tsx +++ b/packages/mui-base/src/Select/Separator/SelectSeparator.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import type { BaseUIComponentProps } from '../../utils/types'; From a1b13e757562244210ff0b92e0813e2ad1d720c4 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 20 Sep 2024 18:07:14 +1000 Subject: [PATCH 72/94] Update packages/mui-base/src/Select/Value/SelectValue.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Dudak Signed-off-by: atomiks --- packages/mui-base/src/Select/Value/SelectValue.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mui-base/src/Select/Value/SelectValue.tsx b/packages/mui-base/src/Select/Value/SelectValue.tsx index 06512e363..074f8ba7e 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.tsx @@ -1,3 +1,4 @@ +'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; import { useSelectRootContext } from '../Root/SelectRootContext'; From 58668107e23ac79e4ee9484eb4aaf79249e057ac Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 20 Sep 2024 18:11:26 +1000 Subject: [PATCH 73/94] Update packages/mui-base/src/Field/Root/FieldRoot.test.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Dudak Signed-off-by: atomiks --- packages/mui-base/src/Field/Root/FieldRoot.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 7204b25bb..5b17c9eef 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -10,8 +10,7 @@ import * as Select from '@base_ui/react/Select'; import userEvent from '@testing-library/user-event'; import { act, fireEvent, flushMicrotasks, screen, waitFor } from '@mui/internal-test-utils'; import { expect } from 'chai'; -import { describeConformance } from '../../../test/describeConformance'; -import { createRenderer } from '../../../test/createRenderer'; +import { createRenderer, describeConformance } from '#test-utils'; const user = userEvent.setup(); From 8e94ee6e5e0b675d015065a7b8be30c66cf33f87 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 24 Sep 2024 15:26:43 +1000 Subject: [PATCH 74/94] Feedback --- docs/src/styles/reset.css | 2 +- docs/src/styles/theme.css | 1 + .../src/Menu/Positioner/useMenuPositioner.ts | 3 ++- packages/mui-base/src/Menu/Root/useMenuRoot.ts | 3 ++- .../mui-base/src/NumberField/Root/useScrub.ts | 3 ++- .../src/Popover/Backdrop/usePopoverBackdrop.ts | 3 ++- .../Popover/Positioner/usePopoverPositioner.tsx | 3 ++- .../Backdrop/usePreviewCardBackdrop.ts | 3 ++- .../Positioner/usePreviewCardPositioner.ts | 3 ++- .../src/Select/Backdrop/useSelectBackdrop.ts | 4 +++- .../src/Select/GroupLabel/SelectGroupLabel.tsx | 16 ++++------------ .../mui-base/src/Select/Popup/SelectPopup.tsx | 4 ++-- .../Select/Positioner/SelectPositionerContext.ts | 11 ++++++++--- .../Select/Positioner/useSelectPositioner.tsx | 3 ++- .../mui-base/src/Select/Root/useSelectRoot.tsx | 3 ++- .../src/Select/ScrollArrow/SelectScrollArrow.tsx | 3 ++- .../Tooltip/Positioner/useTooltipPositioner.ts | 3 ++- packages/mui-base/src/utils/floating.ts | 2 ++ 18 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 packages/mui-base/src/utils/floating.ts diff --git a/docs/src/styles/reset.css b/docs/src/styles/reset.css index f2899ca4a..213ed02b7 100644 --- a/docs/src/styles/reset.css +++ b/docs/src/styles/reset.css @@ -2,7 +2,7 @@ body { margin: 0; /* background-color: var(--gray-surface-2); */ padding-top: 49px; - font-family: 'Inter', sans-serif; + font-family: var(--inter); } *, diff --git a/docs/src/styles/theme.css b/docs/src/styles/theme.css index fd6b22b6d..2bf3f6214 100644 --- a/docs/src/styles/theme.css +++ b/docs/src/styles/theme.css @@ -44,6 +44,7 @@ --br-circle: 50%; --br-pill: 9999px; + --inter: 'Inter', sans-serif; --ff-sans: graphik, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; --ff-code: Söhne mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', diff --git a/packages/mui-base/src/Menu/Positioner/useMenuPositioner.ts b/packages/mui-base/src/Menu/Positioner/useMenuPositioner.ts index 02c1890eb..c83e6324f 100644 --- a/packages/mui-base/src/Menu/Positioner/useMenuPositioner.ts +++ b/packages/mui-base/src/Menu/Positioner/useMenuPositioner.ts @@ -11,6 +11,7 @@ import type { import { mergeReactProps } from '../../utils/mergeReactProps'; import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; import type { GenericHTMLProps } from '../../utils/types'; +import { MAX_Z_INDEX } from '../../utils/floating'; export function useMenuPositioner( params: useMenuPositioner.Parameters, @@ -40,7 +41,7 @@ export function useMenuPositioner( style: { ...positionerStyles, ...hiddenStyles, - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, 'aria-hidden': !open || undefined, inert: !open ? '' : undefined, diff --git a/packages/mui-base/src/Menu/Root/useMenuRoot.ts b/packages/mui-base/src/Menu/Root/useMenuRoot.ts index 84dd3ecda..25fc14eaa 100644 --- a/packages/mui-base/src/Menu/Root/useMenuRoot.ts +++ b/packages/mui-base/src/Menu/Root/useMenuRoot.ts @@ -18,6 +18,7 @@ import { useTransitionStatus } from '../../utils/useTransitionStatus'; import { useEventCallback } from '../../utils/useEventCallback'; import { useAnimationsFinished } from '../../utils/useAnimationsFinished'; import { useControlled } from '../../utils/useControlled'; +import { TYPEAHEAD_RESET_MS } from '../../utils/floating'; const EMPTY_ARRAY: never[] = []; @@ -115,7 +116,7 @@ export function useMenuRoot(parameters: useMenuRoot.Parameters): useMenuRoot.Ret const typeahead = useTypeahead(floatingRootContext, { listRef: itemLabels, activeIndex, - resetMs: 350, + resetMs: TYPEAHEAD_RESET_MS, onMatch: (index) => { if (open && index !== activeIndex) { setActiveIndex(index); diff --git a/packages/mui-base/src/NumberField/Root/useScrub.ts b/packages/mui-base/src/NumberField/Root/useScrub.ts index f3081583f..638c62abe 100644 --- a/packages/mui-base/src/NumberField/Root/useScrub.ts +++ b/packages/mui-base/src/NumberField/Root/useScrub.ts @@ -10,6 +10,7 @@ import { useLatestRef } from '../../utils/useLatestRef'; import { isWebKit } from '../../utils/detectBrowser'; import { mergeReactProps } from '../../utils/mergeReactProps'; import type { UseNumberFieldRoot } from './useNumberFieldRoot'; +import { MAX_Z_INDEX } from '../../utils/floating'; /** * @ignore - internal hook. @@ -173,7 +174,7 @@ export function useScrub(params: ScrubParams) { top: 0, left: 0, pointerEvents: 'none', - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, }, ), diff --git a/packages/mui-base/src/Popover/Backdrop/usePopoverBackdrop.ts b/packages/mui-base/src/Popover/Backdrop/usePopoverBackdrop.ts index 08e5eadb9..2d3668d6f 100644 --- a/packages/mui-base/src/Popover/Backdrop/usePopoverBackdrop.ts +++ b/packages/mui-base/src/Popover/Backdrop/usePopoverBackdrop.ts @@ -1,13 +1,14 @@ import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import type { GenericHTMLProps } from '../../utils/types'; +import { MAX_Z_INDEX } from '../../utils/floating'; export function usePopoverBackdrop(): usePopoverBackdrop.ReturnValue { const getBackdropProps = React.useCallback((externalProps = {}) => { return mergeReactProps<'div'>(externalProps, { role: 'presentation', style: { - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index overflow: 'auto', position: 'fixed', inset: 0, diff --git a/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx b/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx index 4a9c04746..41e054092 100644 --- a/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx +++ b/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx @@ -9,6 +9,7 @@ import type { import { mergeReactProps } from '../../utils/mergeReactProps'; import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; import type { GenericHTMLProps } from '../../utils/types'; +import { MAX_Z_INDEX } from '../../utils/floating'; export function usePopoverPositioner( params: usePopoverPositioner.Parameters, @@ -42,7 +43,7 @@ export function usePopoverPositioner( style: { ...positionerStyles, ...hiddenStyles, - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, }); }, diff --git a/packages/mui-base/src/PreviewCard/Backdrop/usePreviewCardBackdrop.ts b/packages/mui-base/src/PreviewCard/Backdrop/usePreviewCardBackdrop.ts index a6d90254a..a20a8edcc 100644 --- a/packages/mui-base/src/PreviewCard/Backdrop/usePreviewCardBackdrop.ts +++ b/packages/mui-base/src/PreviewCard/Backdrop/usePreviewCardBackdrop.ts @@ -1,12 +1,13 @@ import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; import type { GenericHTMLProps } from '../../utils/types'; +import { MAX_Z_INDEX } from '../../utils/floating'; export function usePreviewCardBackdrop(): usePreviewCardBackdrop.ReturnValue { const getBackdropProps = React.useCallback((externalProps = {}) => { return mergeReactProps<'div'>(externalProps, { style: { - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index overflow: 'auto', position: 'fixed', inset: 0, diff --git a/packages/mui-base/src/PreviewCard/Positioner/usePreviewCardPositioner.ts b/packages/mui-base/src/PreviewCard/Positioner/usePreviewCardPositioner.ts index 30d6ada6d..22feb2aeb 100644 --- a/packages/mui-base/src/PreviewCard/Positioner/usePreviewCardPositioner.ts +++ b/packages/mui-base/src/PreviewCard/Positioner/usePreviewCardPositioner.ts @@ -9,6 +9,7 @@ import type { import { mergeReactProps } from '../../utils/mergeReactProps'; import { useAnchorPositioning, type Side } from '../../utils/useAnchorPositioning'; import type { GenericHTMLProps } from '../../utils/types'; +import { MAX_Z_INDEX } from '../../utils/floating'; export function usePreviewCardPositioner( params: usePreviewCardPositioner.Parameters, @@ -39,7 +40,7 @@ export function usePreviewCardPositioner( style: { ...positionerStyles, ...hiddenStyles, - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, }); }, diff --git a/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts b/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts index 789753981..0831bc938 100644 --- a/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts +++ b/packages/mui-base/src/Select/Backdrop/useSelectBackdrop.ts @@ -1,6 +1,8 @@ 'use client'; import * as React from 'react'; import { mergeReactProps } from '../../utils/mergeReactProps'; +import { MAX_Z_INDEX } from '../../utils/floating'; + /** * * API: @@ -12,7 +14,7 @@ export function useSelectBackdrop() { return mergeReactProps<'div'>(externalProps, { role: 'presentation', style: { - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index overflow: 'auto', position: 'fixed', inset: 0, diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx index ff5902f23..0d54dbd2e 100644 --- a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx @@ -7,7 +7,8 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; import { useId } from '../../utils/useId'; import { useSelectGroupContext } from '../Group/SelectGroupContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; -import { useSelectRootContext } from '../Root/SelectRootContext'; + +const ownerState = {}; /** * @@ -25,16 +26,8 @@ const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( ) { const { className, render, id: idProp, ...otherProps } = props; - const { open } = useSelectRootContext(); const { setLabelId } = useSelectGroupContext(); - const ownerState: SelectGroupLabel.OwnerState = React.useMemo( - () => ({ - open, - }), - [open], - ); - const id = useId(idProp); useEnhancedEffect(() => { @@ -85,9 +78,8 @@ SelectGroupLabel.propTypes /* remove-proptypes */ = { } as any; namespace SelectGroupLabel { - export interface OwnerState { - open: boolean; - } + export interface OwnerState {} + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} } diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.tsx index 1b6e8128f..ea1f1f6e2 100644 --- a/packages/mui-base/src/Select/Popup/SelectPopup.tsx +++ b/packages/mui-base/src/Select/Popup/SelectPopup.tsx @@ -14,10 +14,10 @@ import { useSelectPopup } from './useSelectPopup'; const customStyleHookMapping: CustomStyleHookMapping = { ...commonStyleHooks, entering(value) { - return value ? { 'data-select-entering': '' } : null; + return value ? { 'data-entering': '' } : null; }, exiting(value) { - return value ? { 'data-select-exiting': '' } : null; + return value ? { 'data-exiting': '' } : null; }, }; diff --git a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts index 255636c13..3c28717e6 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts +++ b/packages/mui-base/src/Select/Positioner/SelectPositionerContext.ts @@ -14,7 +14,14 @@ export interface SelectPositionerContext { arrowRef: React.MutableRefObject; arrowUncentered: boolean; arrowStyles: React.CSSProperties; + /** + * Determines if the popup has been positioned. + */ isPositioned: boolean; + /** + * Determines the align offset of the popup such that the trigger value and option value are + * aligned on the x-axis. + */ optionTextOffset: number | null; setOptionTextOffset: React.Dispatch>; } @@ -28,9 +35,7 @@ if (process.env.NODE_ENV !== 'production') { export function useSelectPositionerContext() { const context = React.useContext(SelectPositionerContext); if (context === null) { - throw new Error( - 'Base UI: must be used within the component', - ); + throw new Error('Base UI: SelectPositionerContext is undefined.'); } return context; } diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 7a22bcec5..8572084fb 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -13,6 +13,7 @@ import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; import { mergeReactProps } from '../../utils/mergeReactProps'; import { useSelectRootContext } from '../Root/SelectRootContext'; import { useScrollLock } from '../../utils/useScrollLock'; +import { MAX_Z_INDEX } from '../../utils/floating'; /** * @@ -68,7 +69,7 @@ export function useSelectPositioner( style: { ...positionerStyles, ...hiddenStyles, - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, }); }, diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index b18448403..d043809d6 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -23,6 +23,7 @@ import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; import { warn } from '../../utils/warn'; +import { TYPEAHEAD_RESET_MS } from '../../utils/floating'; /** * @@ -173,7 +174,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R const typeahead = useTypeahead(floatingRootContext, { listRef: labelsRef, activeIndex, - resetMs: 500, + resetMs: TYPEAHEAD_RESET_MS, onMatch: open ? setActiveIndex : (index) => { diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index fe88b2d23..f3bb27b57 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -8,6 +8,7 @@ import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useSelectPositionerContext } from '../Positioner/SelectPositionerContext'; import { useEventCallback } from '../../utils/useEventCallback'; import { ownerWindow } from '../../utils/owner'; +import { MAX_Z_INDEX } from '../../utils/floating'; /** * @ignore - internal component. @@ -48,7 +49,7 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( children: direction === 'down' ? '▼' : '▲', style: { position: 'absolute', - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, onMouseEnter() { if (inert) { diff --git a/packages/mui-base/src/Tooltip/Positioner/useTooltipPositioner.ts b/packages/mui-base/src/Tooltip/Positioner/useTooltipPositioner.ts index d733a0af7..0fb376fcb 100644 --- a/packages/mui-base/src/Tooltip/Positioner/useTooltipPositioner.ts +++ b/packages/mui-base/src/Tooltip/Positioner/useTooltipPositioner.ts @@ -3,6 +3,7 @@ import type { Boundary, Padding, VirtualElement, FloatingRootContext } from '@fl import { mergeReactProps } from '../../utils/mergeReactProps'; import { useAnchorPositioning } from '../../utils/useAnchorPositioning'; import type { GenericHTMLProps } from '../../utils/types'; +import { MAX_Z_INDEX } from '../../utils/floating'; export function useTooltipPositioner( params: useTooltipPositioner.Parameters, @@ -39,7 +40,7 @@ export function useTooltipPositioner( ...hiddenStyles, maxWidth: 'var(--available-width)', maxHeight: 'var(--available-height)', - zIndex: 2147483647, // max z-index + zIndex: MAX_Z_INDEX, // max z-index }, }); }, diff --git a/packages/mui-base/src/utils/floating.ts b/packages/mui-base/src/utils/floating.ts new file mode 100644 index 000000000..596450a1b --- /dev/null +++ b/packages/mui-base/src/utils/floating.ts @@ -0,0 +1,2 @@ +export const TYPEAHEAD_RESET_MS = 500; +export const MAX_Z_INDEX = 2147483647; From 077ff0b6e2222871ddda7ad4b26df8ac2620e6fb Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 1 Oct 2024 10:54:41 +1000 Subject: [PATCH 75/94] Generic Value type --- packages/mui-base/src/Select/Root/SelectRoot.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index 9d8b3e59f..85f0ea69a 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { SelectRootContext } from './SelectRootContext'; import { useSelectRoot } from './useSelectRoot'; + /** * * Demos: @@ -13,7 +14,7 @@ import { useSelectRoot } from './useSelectRoot'; * * - [SelectRoot API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectRoot) */ -function SelectRoot(props: SelectRoot.Props) { +function SelectRoot(props: SelectRoot.Props) { const { animated = true, id, @@ -62,7 +63,7 @@ function SelectRoot(props: SelectRoot.Props) { } namespace SelectRoot { - export interface Props { + export interface Props { /** * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. @@ -98,15 +99,15 @@ namespace SelectRoot { /** * The value of the select. */ - value?: any; + value?: Value; /** * Callback fired when the value of the select changes. Use when controlled. */ - onValueChange?: (value: any, event?: Event) => void; + onValueChange?: (value: Value, event?: Event) => void; /** * The default value of the select. */ - defaultValue?: any; + defaultValue?: Value; /** * If `true`, the Select is initially open. * From 493755edd0d0005e533bf7a721fa5cbe542cb6d3 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 1 Oct 2024 10:58:55 +1000 Subject: [PATCH 76/94] Use Separator Root --- .../translations/api-docs/no-ssr/no-ssr.json | 2 +- .../select-separator/select-separator.json | 10 --- packages/mui-base/src/NoSsr/NoSsr.tsx | 8 +-- .../Select/Separator/SelectSeparator.test.tsx | 18 ----- .../src/Select/Separator/SelectSeparator.tsx | 70 ------------------- packages/mui-base/src/Select/index.barrel.ts | 2 +- packages/mui-base/src/Select/index.ts | 2 +- 7 files changed, 7 insertions(+), 105 deletions(-) delete mode 100644 docs/data/translations/api-docs/select-separator/select-separator.json delete mode 100644 packages/mui-base/src/Select/Separator/SelectSeparator.test.tsx delete mode 100644 packages/mui-base/src/Select/Separator/SelectSeparator.tsx diff --git a/docs/data/translations/api-docs/no-ssr/no-ssr.json b/docs/data/translations/api-docs/no-ssr/no-ssr.json index 1d73722e5..ff903d12a 100644 --- a/docs/data/translations/api-docs/no-ssr/no-ssr.json +++ b/docs/data/translations/api-docs/no-ssr/no-ssr.json @@ -1,5 +1,5 @@ { - "componentDescription": "NoSsr purposely removes components from the subject of Server Side Rendering (SSR).\n\nThis component can be useful in a variety of situations:\n\n* Escape hatch for broken dependencies not supporting SSR.\n* Improve the time-to-first paint on the client by only rendering above the fold.\n* Reduce the rendering time on the server.\n* Under too heavy server load, you can turn on service degradation.", + "componentDescription": "NoSsr purposely removes components from the subject of Server Side Rendering (SSR).\n\nThis component can be useful in a variety of situations:\n\n* Escape hatch for broken dependencies not supporting SSR.\n* Improve the time-to-first paint on the client by only rendering above the fold.\n* Reduce the rendering time on the server.\n* Under too heavy server load, you can turn on service degradation.", "propDescriptions": { "children": { "description": "You can wrap a node." }, "defer": { diff --git a/docs/data/translations/api-docs/select-separator/select-separator.json b/docs/data/translations/api-docs/select-separator/select-separator.json deleted file mode 100644 index 4bc12cf1e..000000000 --- a/docs/data/translations/api-docs/select-separator/select-separator.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/packages/mui-base/src/NoSsr/NoSsr.tsx b/packages/mui-base/src/NoSsr/NoSsr.tsx index 9a472f834..50e936efb 100644 --- a/packages/mui-base/src/NoSsr/NoSsr.tsx +++ b/packages/mui-base/src/NoSsr/NoSsr.tsx @@ -9,10 +9,10 @@ import { NoSsrProps } from './NoSsr.types'; * * This component can be useful in a variety of situations: * - * * Escape hatch for broken dependencies not supporting SSR. - * * Improve the time-to-first paint on the client by only rendering above the fold. - * * Reduce the rendering time on the server. - * * Under too heavy server load, you can turn on service degradation. + * * Escape hatch for broken dependencies not supporting SSR. + * * Improve the time-to-first paint on the client by only rendering above the fold. + * * Reduce the rendering time on the server. + * * Under too heavy server load, you can turn on service degradation. * * Demos: * diff --git a/packages/mui-base/src/Select/Separator/SelectSeparator.test.tsx b/packages/mui-base/src/Select/Separator/SelectSeparator.test.tsx deleted file mode 100644 index b91c1906c..000000000 --- a/packages/mui-base/src/Select/Separator/SelectSeparator.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; -import { createRenderer, describeConformance } from '#test-utils'; - -describe('', () => { - const { render } = createRenderer(); - - describeConformance(, () => ({ - refInstanceof: window.HTMLDivElement, - render(node) { - return render( - - {node} - , - ); - }, - })); -}); diff --git a/packages/mui-base/src/Select/Separator/SelectSeparator.tsx b/packages/mui-base/src/Select/Separator/SelectSeparator.tsx deleted file mode 100644 index 870e8cc09..000000000 --- a/packages/mui-base/src/Select/Separator/SelectSeparator.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import type { BaseUIComponentProps } from '../../utils/types'; -import { useComponentRenderer } from '../../utils/useComponentRenderer'; -import { mergeReactProps } from '../../utils/mergeReactProps'; -/** - * - * Demos: - * - * - [Select](https://base-ui.netlify.app/components/react-select/) - * - * API: - * - * - [SelectSeparator API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectSeparator) - */ -const SelectSeparator = React.forwardRef(function SelectSeparator( - props: SelectSeparator.Props, - forwardedRef: React.ForwardedRef, -) { - const { className, render, ...otherProps } = props; - - const ownerState: SelectSeparator.OwnerState = React.useMemo(() => ({}), []); - - const getSeparatorProps = React.useCallback( - (externalProps = {}) => - mergeReactProps<'div'>(externalProps, { - role: 'separator', - 'aria-hidden': true, - }), - [], - ); - - const { renderElement } = useComponentRenderer({ - propGetter: getSeparatorProps, - ref: forwardedRef, - render: render ?? 'div', - className, - ownerState, - extraProps: otherProps, - }); - - return renderElement(); -}); - -SelectSeparator.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * Class names applied to the element or a function that returns them based on the component's state. - */ - className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - /** - * A function to customize rendering of the component. - */ - render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), -} as any; - -namespace SelectSeparator { - export interface OwnerState {} - export interface Props extends BaseUIComponentProps<'div', OwnerState> {} -} - -export { SelectSeparator }; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts index 21c50374f..0e1b00dcb 100644 --- a/packages/mui-base/src/Select/index.barrel.ts +++ b/packages/mui-base/src/Select/index.barrel.ts @@ -10,7 +10,7 @@ export { SelectGroupLabel } from './GroupLabel/SelectGroupLabel'; export { SelectValue } from './Value/SelectValue'; export { SelectScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; export { SelectScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; -export { SelectSeparator } from './Separator/SelectSeparator'; +export { SeparatorRoot as SelectSeparator } from '../Separator/Root/SeparatorRoot'; export { SelectIcon } from './Icon/SelectIcon'; export { SelectArrow } from './Arrow/SelectArrow'; export { SelectOptionText } from './OptionText/SelectOptionText'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index ed212ac3b..56779ec33 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -10,7 +10,7 @@ export { SelectGroupLabel as GroupLabel } from './GroupLabel/SelectGroupLabel'; export { SelectValue as Value } from './Value/SelectValue'; export { SelectScrollUpArrow as ScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; -export { SelectSeparator as Separator } from './Separator/SelectSeparator'; +export { SeparatorRoot as Separator } from '../Separator/Root/SeparatorRoot'; export { SelectIcon as Icon } from './Icon/SelectIcon'; export { SelectArrow as Arrow } from './Arrow/SelectArrow'; export { SelectOptionText as OptionText } from './OptionText/SelectOptionText'; From 06c22a5c75726a51e6bdd30f8acd597b4f64fdf8 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 2 Oct 2024 10:22:47 +1000 Subject: [PATCH 77/94] Update --- docs/data/api/select-root.json | 5 +-- .../select/SelectIntroduction/system/index.js | 2 +- .../SelectIntroduction/system/index.tsx | 2 +- docs/data/components/select/select.mdx | 23 +++++----- .../translations/api-docs/no-ssr/no-ssr.json | 2 +- .../api-docs/select-root/select-root.json | 4 +- .../api-docs/select-arrow/select-arrow.json | 13 ------ .../select-backdrop/select-backdrop.json | 14 ------ .../api-docs/select-icon/select-icon.json | 10 ----- .../select-option-group-label.json | 10 ----- .../select-option-group.json | 10 ----- .../select-option-indicator.json | 13 ------ .../api-docs/select-option/select-option.json | 12 ------ .../api-docs/select-popup/select-popup.json | 11 ----- .../select-positioner/select-positioner.json | 43 ------------------- .../api-docs/select-root/select-root.json | 29 ------------- .../select-scroll-down-arrow.json | 9 ---- .../select-scroll-up-arrow.json | 9 ---- .../select-separator/select-separator.json | 10 ----- .../select-trigger/select-trigger.json | 15 ------- .../api-docs/select-value/select-value.json | 9 ---- .../use-select-backdrop.json | 1 - .../use-select-option/use-select-option.json | 1 - .../use-select-popup/use-select-popup.json | 1 - .../use-select-positioner.json | 1 - .../use-select-root/use-select-root.json | 1 - .../use-select-trigger.json | 1 - packages/mui-base/src/NoSsr/NoSsr.tsx | 8 ++-- .../src/Select/Arrow/SelectArrow.test.tsx | 2 +- .../mui-base/src/Select/Arrow/SelectArrow.tsx | 4 +- .../Select/OptionText/SelectOptionText.tsx | 6 +-- .../src/Select/Popup/useSelectPopup.ts | 6 +-- .../Select/Positioner/SelectPositioner.tsx | 6 +-- .../Select/Positioner/useSelectPositioner.tsx | 10 ++--- .../mui-base/src/Select/Root/SelectRoot.tsx | 22 +++++----- .../src/Select/Root/SelectRootContext.ts | 2 +- .../src/Select/Root/useSelectRoot.tsx | 10 ++--- .../Select/ScrollArrow/SelectScrollArrow.tsx | 12 ++++-- packages/mui-base/src/Test/index.tsx | 13 ++++++ 39 files changed, 74 insertions(+), 288 deletions(-) delete mode 100644 docs/translations/api-docs/select-arrow/select-arrow.json delete mode 100644 docs/translations/api-docs/select-backdrop/select-backdrop.json delete mode 100644 docs/translations/api-docs/select-icon/select-icon.json delete mode 100644 docs/translations/api-docs/select-option-group-label/select-option-group-label.json delete mode 100644 docs/translations/api-docs/select-option-group/select-option-group.json delete mode 100644 docs/translations/api-docs/select-option-indicator/select-option-indicator.json delete mode 100644 docs/translations/api-docs/select-option/select-option.json delete mode 100644 docs/translations/api-docs/select-popup/select-popup.json delete mode 100644 docs/translations/api-docs/select-positioner/select-positioner.json delete mode 100644 docs/translations/api-docs/select-root/select-root.json delete mode 100644 docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json delete mode 100644 docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json delete mode 100644 docs/translations/api-docs/select-separator/select-separator.json delete mode 100644 docs/translations/api-docs/select-trigger/select-trigger.json delete mode 100644 docs/translations/api-docs/select-value/select-value.json delete mode 100644 docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json delete mode 100644 docs/translations/api-docs/use-select-option/use-select-option.json delete mode 100644 docs/translations/api-docs/use-select-popup/use-select-popup.json delete mode 100644 docs/translations/api-docs/use-select-positioner/use-select-positioner.json delete mode 100644 docs/translations/api-docs/use-select-root/use-select-root.json delete mode 100644 docs/translations/api-docs/use-select-trigger/use-select-trigger.json create mode 100644 packages/mui-base/src/Test/index.tsx diff --git a/docs/data/api/select-root.json b/docs/data/api/select-root.json index 88a23449a..8eb3aad7a 100644 --- a/docs/data/api/select-root.json +++ b/docs/data/api/select-root.json @@ -1,9 +1,6 @@ { "props": { - "alignMethod": { - "type": { "name": "enum", "description": "'item'
            | 'trigger'" }, - "default": "'item'" - }, + "alignOptionToTrigger": { "type": { "name": "bool" }, "default": "true" }, "animated": { "type": { "name": "bool" }, "default": "true" }, "defaultOpen": { "type": { "name": "bool" }, "default": "false" }, "defaultValue": { "type": { "name": "any" } }, diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 3b51d396d..58c72be26 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -6,7 +6,7 @@ import { css, styled } from '@mui/system'; export default function SelectIntroduction() { return ( - + console.log(v)}> diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 4710badad..7137afae7 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -6,7 +6,7 @@ import { css, styled } from '@mui/system'; export default function SelectIntroduction() { return ( - + console.log(v)}> diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index bb3bc63c3..e810b40e9 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -2,7 +2,7 @@ productId: base-ui title: React Select component description: Select provides users with a floating element containing a list of options to choose from. -components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionText, SelectOptionIndicator, SelectGroup, SelectGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectSeparator, SelectArrow +components: SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectBackdrop, SelectPositioner, SelectPopup, SelectOption, SelectOptionText, SelectOptionIndicator, SelectGroup, SelectGroupLabel, SelectScrollUpArrow, SelectScrollDownArrow, SelectArrow githubLabel: 'component: select' waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/ --- @@ -37,10 +37,10 @@ Selects are implemented using a collection of related components: - `` renders an option indicator inside an option to indicate it's selected (e.g. a check icon). - `` renders an group for a set of options, wrapping `` components. - `` renders a label for a group of options. -- `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. -- `` renders a scrolling arrow for the `alignMethod="item"` anchoring mode. +- `` renders a scrolling arrow for the `alignOptionToTrigger` anchoring mode. +- `` renders a scrolling arrow for the `alignOptionToTrigger` anchoring mode. - `` renders a separator between option groups. -- `` renders the select popup's arrow when using `alignMethod="trigger"`. +- `` renders the select popup's arrow when disabling `alignOptionToTrigger`. ```jsx @@ -138,21 +138,18 @@ The `[data-selected]` attribute is added to the subcomponent when its owning opt ## Align method -Two different methods to align the popup are available: - -- `alignMethod="item"` (default) -- `alignMethod="trigger"` +By default, the selected option inside the popup is aligned to the trigger element. This can be disabled with the `alignOptionToTrigger` prop: ```jsx - + ``` -- **`item`**: aligns the popup such that the selected option inside of it appears centered over the trigger. If there's not enough space, it falls back to `trigger` anchoring. This method is useful as it allows the user to select the an option in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS. The scroll arrow components must be used to ensure this is the case. -- **`trigger`**: aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. +- **`alignOptionToTrigger`**: aligns the popup such that the selected option inside of it appears centered over the trigger. If there's not enough space, it falls back to standard anchoring. This method is useful as it allows the user to select the an option in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS; the scroll arrow components must be used to ensure a single pointer cycle can be used. +- **`alignOptionToTrigger={false}`**: aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. ### Scrollable popup -When using `alignMethod="trigger"`, the select's height needs to be manually limited by its available space using CSS. +When disabling `alignOptionToTrigger`, the select's height needs to be manually limited by its available space using CSS. This can be achieved by using the `--available-height` CSS variable: @@ -188,7 +185,7 @@ A function can be specified as a child to customize the rendering of the value: To add an arrow (caret or triangle) inside the select popup that points toward the center of the anchor element, use the `Select.Arrow` component: -Note: this is only supported when using `alignMethod="trigger"`. +Note: this is only supported when disabling `alignOptionToTrigger`. ```jsx diff --git a/docs/data/translations/api-docs/no-ssr/no-ssr.json b/docs/data/translations/api-docs/no-ssr/no-ssr.json index ff903d12a..1d73722e5 100644 --- a/docs/data/translations/api-docs/no-ssr/no-ssr.json +++ b/docs/data/translations/api-docs/no-ssr/no-ssr.json @@ -1,5 +1,5 @@ { - "componentDescription": "NoSsr purposely removes components from the subject of Server Side Rendering (SSR).\n\nThis component can be useful in a variety of situations:\n\n* Escape hatch for broken dependencies not supporting SSR.\n* Improve the time-to-first paint on the client by only rendering above the fold.\n* Reduce the rendering time on the server.\n* Under too heavy server load, you can turn on service degradation.", + "componentDescription": "NoSsr purposely removes components from the subject of Server Side Rendering (SSR).\n\nThis component can be useful in a variety of situations:\n\n* Escape hatch for broken dependencies not supporting SSR.\n* Improve the time-to-first paint on the client by only rendering above the fold.\n* Reduce the rendering time on the server.\n* Under too heavy server load, you can turn on service degradation.", "propDescriptions": { "children": { "description": "You can wrap a node." }, "defer": { diff --git a/docs/data/translations/api-docs/select-root/select-root.json b/docs/data/translations/api-docs/select-root/select-root.json index 9e50b677f..920a7a692 100644 --- a/docs/data/translations/api-docs/select-root/select-root.json +++ b/docs/data/translations/api-docs/select-root/select-root.json @@ -1,8 +1,8 @@ { "componentDescription": "", "propDescriptions": { - "alignMethod": { - "description": "Determines if the select should align to the selected item inside the popup or the trigger element." + "alignOptionToTrigger": { + "description": "Determines if the selected option inside the popup should align to the trigger element." }, "animated": { "description": "If true, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." diff --git a/docs/translations/api-docs/select-arrow/select-arrow.json b/docs/translations/api-docs/select-arrow/select-arrow.json deleted file mode 100644 index 3dfaaeafb..000000000 --- a/docs/translations/api-docs/select-arrow/select-arrow.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "hideWhenUncentered": { - "description": "If true, the arrow is hidden when it can't point to the center of the anchor element." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-backdrop/select-backdrop.json b/docs/translations/api-docs/select-backdrop/select-backdrop.json deleted file mode 100644 index 17b35bc88..000000000 --- a/docs/translations/api-docs/select-backdrop/select-backdrop.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "container": { "description": "The container element to which the Backdrop is appended to." }, - "keepMounted": { - "description": "If true, the Backdrop remains mounted when the Select popup is closed." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-icon/select-icon.json b/docs/translations/api-docs/select-icon/select-icon.json deleted file mode 100644 index 4bc12cf1e..000000000 --- a/docs/translations/api-docs/select-icon/select-icon.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-option-group-label/select-option-group-label.json b/docs/translations/api-docs/select-option-group-label/select-option-group-label.json deleted file mode 100644 index 4bc12cf1e..000000000 --- a/docs/translations/api-docs/select-option-group-label/select-option-group-label.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-option-group/select-option-group.json b/docs/translations/api-docs/select-option-group/select-option-group.json deleted file mode 100644 index 4bc12cf1e..000000000 --- a/docs/translations/api-docs/select-option-group/select-option-group.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-option-indicator/select-option-indicator.json b/docs/translations/api-docs/select-option-indicator/select-option-indicator.json deleted file mode 100644 index 9c4340d03..000000000 --- a/docs/translations/api-docs/select-option-indicator/select-option-indicator.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "keepMounted": { - "description": "If true, the item indicator remains mounted when the item is not selected." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-option/select-option.json b/docs/translations/api-docs/select-option/select-option.json deleted file mode 100644 index b639a9b4a..000000000 --- a/docs/translations/api-docs/select-option/select-option.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "componentDescription": "An unstyled select item to be used within a Select.", - "propDescriptions": { - "disabled": { "description": "If true, the select option will be disabled." }, - "label": { - "description": "A text representation of the select option's content. Used for keyboard text navigation matching." - }, - "onClick": { "description": "The click handler for the select option." }, - "value": { "description": "The value of the select option." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-popup/select-popup.json b/docs/translations/api-docs/select-popup/select-popup.json deleted file mode 100644 index 4a1c0a206..000000000 --- a/docs/translations/api-docs/select-popup/select-popup.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "id": { "description": "The id of the popup element." }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-positioner/select-positioner.json b/docs/translations/api-docs/select-positioner/select-positioner.json deleted file mode 100644 index 410bc02b7..000000000 --- a/docs/translations/api-docs/select-positioner/select-positioner.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "componentDescription": "Renders the element that positions the Select popup.", - "propDescriptions": { - "alignment": { - "description": "The alignment of the Select element to the anchor element along its cross axis." - }, - "alignmentOffset": { - "description": "The offset of the Select element along its alignment axis." - }, - "anchor": { "description": "The anchor element to which the Select popup will be placed at." }, - "arrowPadding": { - "description": "Determines the padding between the arrow and the Select popup's edges. Useful when the popover popup has rounded corners via border-radius." - }, - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "collisionBoundary": { - "description": "The boundary that the Select element should be constrained to." - }, - "collisionPadding": { "description": "The padding of the collision boundary." }, - "container": { - "description": "The container element to which the Select popup will be appended to." - }, - "hideWhenDetached": { - "description": "If true, the Select will be hidden if it is detached from its anchor element due to differing clipping contexts." - }, - "keepMounted": { - "description": "Whether the select popup remains mounted in the DOM while closed." - }, - "positionStrategy": { - "description": "The CSS position strategy for positioning the Select popup element." - }, - "render": { "description": "A function to customize rendering of the component." }, - "side": { - "description": "The side of the anchor element that the Select element should align to." - }, - "sideOffset": { "description": "The gap between the anchor element and the Select element." }, - "sticky": { - "description": "If true, allow the Select to remain in stuck view while the anchor element is scrolled out of view." - } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-root/select-root.json b/docs/translations/api-docs/select-root/select-root.json deleted file mode 100644 index df1f10fce..000000000 --- a/docs/translations/api-docs/select-root/select-root.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "alignMethod": { - "description": "Determines if the select should align to the selected item inside the popup or the trigger element." - }, - "animated": { - "description": "If true, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes." - }, - "defaultOpen": { "description": "If true, the Select is initially open." }, - "defaultValue": { "description": "The default value of the select." }, - "disabled": { "description": "If true, the Select is disabled." }, - "id": { "description": "The id of the Select." }, - "loop": { - "description": "If true, using keyboard navigation will wrap focus to the other end of the list once the end is reached." - }, - "name": { "description": "The name of the Select in the owning form." }, - "onOpenChange": { - "description": "Callback fired when the component requests to be opened or closed." - }, - "open": { - "description": "Allows to control whether the dropdown is open. This is a controlled counterpart of defaultOpen." - }, - "readOnly": { "description": "If true, the Select is read-only." }, - "required": { "description": "If true, the Select is required." }, - "value": { "description": "The value of the select." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json b/docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json deleted file mode 100644 index 47e423952..000000000 --- a/docs/translations/api-docs/select-scroll-down-arrow/select-scroll-down-arrow.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "keepMounted": { - "description": "Whether the component should be kept mounted when it is not rendered." - } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json b/docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json deleted file mode 100644 index 47e423952..000000000 --- a/docs/translations/api-docs/select-scroll-up-arrow/select-scroll-up-arrow.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "keepMounted": { - "description": "Whether the component should be kept mounted when it is not rendered." - } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-separator/select-separator.json b/docs/translations/api-docs/select-separator/select-separator.json deleted file mode 100644 index 4bc12cf1e..000000000 --- a/docs/translations/api-docs/select-separator/select-separator.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-trigger/select-trigger.json b/docs/translations/api-docs/select-trigger/select-trigger.json deleted file mode 100644 index 814aa3e49..000000000 --- a/docs/translations/api-docs/select-trigger/select-trigger.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "className": { - "description": "Class names applied to the element or a function that returns them based on the component's state." - }, - "disabled": { "description": "If true, the component is disabled." }, - "focusableWhenDisabled": { - "description": "If true, allows a disabled button to receive focus." - }, - "label": { "description": "Label of the button" }, - "render": { "description": "A function to customize rendering of the component." } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/select-value/select-value.json b/docs/translations/api-docs/select-value/select-value.json deleted file mode 100644 index 491cf0963..000000000 --- a/docs/translations/api-docs/select-value/select-value.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "placeholder": { - "description": "The placeholder value to display when the value is empty (such as during SSR)." - } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json b/docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json deleted file mode 100644 index e3eb65c6e..000000000 --- a/docs/translations/api-docs/use-select-backdrop/use-select-backdrop.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-option/use-select-option.json b/docs/translations/api-docs/use-select-option/use-select-option.json deleted file mode 100644 index e3eb65c6e..000000000 --- a/docs/translations/api-docs/use-select-option/use-select-option.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-popup/use-select-popup.json b/docs/translations/api-docs/use-select-popup/use-select-popup.json deleted file mode 100644 index e3eb65c6e..000000000 --- a/docs/translations/api-docs/use-select-popup/use-select-popup.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-positioner/use-select-positioner.json b/docs/translations/api-docs/use-select-positioner/use-select-positioner.json deleted file mode 100644 index e3eb65c6e..000000000 --- a/docs/translations/api-docs/use-select-positioner/use-select-positioner.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-root/use-select-root.json b/docs/translations/api-docs/use-select-root/use-select-root.json deleted file mode 100644 index e3eb65c6e..000000000 --- a/docs/translations/api-docs/use-select-root/use-select-root.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/docs/translations/api-docs/use-select-trigger/use-select-trigger.json b/docs/translations/api-docs/use-select-trigger/use-select-trigger.json deleted file mode 100644 index e3eb65c6e..000000000 --- a/docs/translations/api-docs/use-select-trigger/use-select-trigger.json +++ /dev/null @@ -1 +0,0 @@ -{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} } diff --git a/packages/mui-base/src/NoSsr/NoSsr.tsx b/packages/mui-base/src/NoSsr/NoSsr.tsx index 50e936efb..9a472f834 100644 --- a/packages/mui-base/src/NoSsr/NoSsr.tsx +++ b/packages/mui-base/src/NoSsr/NoSsr.tsx @@ -9,10 +9,10 @@ import { NoSsrProps } from './NoSsr.types'; * * This component can be useful in a variety of situations: * - * * Escape hatch for broken dependencies not supporting SSR. - * * Improve the time-to-first paint on the client by only rendering above the fold. - * * Reduce the rendering time on the server. - * * Under too heavy server load, you can turn on service degradation. + * * Escape hatch for broken dependencies not supporting SSR. + * * Improve the time-to-first paint on the client by only rendering above the fold. + * * Reduce the rendering time on the server. + * * Under too heavy server load, you can turn on service degradation. * * Demos: * diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx index 4edde331d..54de5be14 100644 --- a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx @@ -9,7 +9,7 @@ describe('', () => { refInstanceof: window.HTMLDivElement, render(node) { return render( - + {node} , ); diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx index b30bcf151..4602a5373 100644 --- a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx @@ -24,7 +24,7 @@ const SelectArrow = React.forwardRef(function SelectArrow( ) { const { className, render, hideWhenUncentered = false, ...otherProps } = props; - const { open, alignMethod } = useSelectRootContext(); + const { open, alignOptionToTrigger } = useSelectRootContext(); const { arrowRef, side, alignment, arrowUncentered, arrowStyles } = useSelectPositionerContext(); const getArrowProps = React.useCallback( @@ -60,7 +60,7 @@ const SelectArrow = React.forwardRef(function SelectArrow( customStyleHookMapping: commonStyleHooks, }); - if (alignMethod !== 'trigger') { + if (alignOptionToTrigger) { return null; } diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index 19715ee39..f22468b63 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -25,7 +25,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( ) { const { className, render, ...otherProps } = props; - const { open, triggerElement, valueRef, popupRef, innerFallback, alignMethod } = + const { open, triggerElement, valueRef, popupRef, innerFallback, alignOptionToTrigger } = useSelectRootContext(); const { isPositioned, setOptionTextOffset } = useSelectPositionerContext(); const { selected } = useSelectOptionContext(); @@ -37,7 +37,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( useEnhancedEffect(() => { if ( - alignMethod !== 'item' || + !alignOptionToTrigger || innerFallback || !open || !isPositioned || @@ -60,7 +60,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( setOptionTextOffset(triggerDiff - popupDiff); }, [ - alignMethod, + alignOptionToTrigger, innerFallback, open, isPositioned, diff --git a/packages/mui-base/src/Select/Popup/useSelectPopup.ts b/packages/mui-base/src/Select/Popup/useSelectPopup.ts index 62c2f0d85..d61bfb457 100644 --- a/packages/mui-base/src/Select/Popup/useSelectPopup.ts +++ b/packages/mui-base/src/Select/Popup/useSelectPopup.ts @@ -14,7 +14,7 @@ import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; export function useSelectPopup(): useSelectPopup.ReturnValue { const { getPopupProps: getRootPopupProps, - alignMethod, + alignOptionToTrigger, selectedIndex, touchModality, } = useSelectRootContext(); @@ -30,7 +30,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { return mergeReactProps<'div'>(getRootPopupProps(externalProps), { style: { ...(pointerEvents === 'none' && { pointerEvents }), - ...(alignMethod === 'item' && + ...(alignOptionToTrigger && hasSelectedIndex && !touchModality && { // Note: not supported in Safari. Needs to be manually specified in CSS. @@ -39,7 +39,7 @@ export function useSelectPopup(): useSelectPopup.ReturnValue { }, }); }, - [getRootPopupProps, pointerEvents, alignMethod, hasSelectedIndex, touchModality], + [getRootPopupProps, pointerEvents, alignOptionToTrigger, hasSelectedIndex, touchModality], ); useEnhancedEffect(() => { diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index 28d33043f..c86b3346a 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -65,7 +65,6 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( popupRef, overflowRef, innerOffset, - alignMethod, innerFallback, setInnerFallback, selectedIndex, @@ -78,6 +77,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( inputRef, getInputValidationProps, touchModality, + alignOptionToTrigger, } = useSelectRootContext(); const { setControlId, validityData, setDirty } = useFieldRootContext(); @@ -135,7 +135,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( allowAxisFlip: false, innerFallback, inner: - alignMethod === 'item' && selectedIndexOnMount !== null + alignOptionToTrigger && selectedIndexOnMount !== null ? // Dependency-injected for tree-shaking purposes. Other floating element components don't // use or need this. inner({ @@ -151,7 +151,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( popupRef.current.style.maxHeight = ''; } }, - minItemsVisible: touchModality ? 8 : 4, + minItemsVisible: touchModality ? 8 : 2.5, referenceOverflowThreshold: 20, overflowRef, }) diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 8572084fb..6f973c3e6 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -26,9 +26,9 @@ export function useSelectPositioner( ): useSelectPositioner.ReturnValue { const { open = false, keepMounted } = params; - const { touchModality, alignMethod, innerFallback, mounted } = useSelectRootContext(); + const { touchModality, alignOptionToTrigger, innerFallback, mounted } = useSelectRootContext(); - const itemAligned = alignMethod === 'item' && !innerFallback && !touchModality; + const itemAligned = alignOptionToTrigger && !innerFallback && !touchModality; useScrollLock(itemAligned && mounted); @@ -82,7 +82,7 @@ export function useSelectPositioner( arrowRef, arrowUncentered, arrowStyles, - side: alignMethod === 'item' && !innerFallback ? 'none' : renderedSide, + side: alignOptionToTrigger && !innerFallback ? 'none' : renderedSide, alignment: renderedAlignment, floatingContext, isPositioned, @@ -92,7 +92,7 @@ export function useSelectPositioner( arrowRef, arrowUncentered, arrowStyles, - alignMethod, + alignOptionToTrigger, innerFallback, renderedSide, renderedAlignment, @@ -196,7 +196,7 @@ export namespace useSelectPositioner { */ nodeId?: string; /** - * If specified, positions the popup relative to the selected item inside it. + * If specified, positions the popup relative to the selected option inside it. */ inner?: Middleware; /** diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index 85f0ea69a..de0e0e86c 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -30,7 +30,7 @@ function SelectRoot(props: SelectRoot.Props) { loop = true, onOpenChange, open, - alignMethod = 'item', + alignOptionToTrigger = true, } = props; const selectRoot = useSelectRoot({ @@ -40,23 +40,23 @@ function SelectRoot(props: SelectRoot.Props) { loop, defaultOpen, open, - alignMethod, defaultValue, value, onValueChange, + alignOptionToTrigger, }); const context: SelectRootContext = React.useMemo( () => ({ ...selectRoot, disabled, - alignMethod, id, name, required, readOnly, + alignOptionToTrigger, }), - [selectRoot, disabled, alignMethod, id, name, required, readOnly], + [selectRoot, disabled, id, name, required, readOnly, alignOptionToTrigger], ); return {children}; @@ -129,11 +129,10 @@ namespace SelectRoot { */ open?: boolean; /** - * Determines if the select should align to the selected item inside the popup or the trigger - * element. - * @default 'item' + * Determines if the selected option inside the popup should align to the trigger element. + * @default true */ - alignMethod?: 'item' | 'trigger'; + alignOptionToTrigger?: boolean; } } @@ -143,11 +142,10 @@ SelectRoot.propTypes /* remove-proptypes */ = { // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** - * Determines if the select should align to the selected item inside the popup or the trigger - * element. - * @default 'item' + * Determines if the selected option inside the popup should align to the trigger element. + * @default true */ - alignMethod: PropTypes.oneOf(['item', 'trigger']), + alignOptionToTrigger: PropTypes.bool, /** * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. diff --git a/packages/mui-base/src/Select/Root/SelectRootContext.ts b/packages/mui-base/src/Select/Root/SelectRootContext.ts index dedbcc109..7a846f63a 100644 --- a/packages/mui-base/src/Select/Root/SelectRootContext.ts +++ b/packages/mui-base/src/Select/Root/SelectRootContext.ts @@ -7,7 +7,7 @@ export interface SelectRootContext extends useSelectRoot.ReturnValue, useFieldControlValidation.ReturnValue { typingRef: React.MutableRefObject; - alignMethod: 'item' | 'trigger'; + alignOptionToTrigger: boolean; id: string | undefined; name: string | undefined; disabled: boolean; diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index d043809d6..91bfedcca 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -42,7 +42,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R value: valueProp, onValueChange, defaultValue = null, - alignMethod, + alignOptionToTrigger, } = params; const { setDirty, validityData, validateOnChange } = useFieldRootContext(); @@ -186,7 +186,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R }); const innerOffsetInteractionProps = useInnerOffset(floatingRootContext, { - enabled: alignMethod === 'item' && !innerFallback, + enabled: alignOptionToTrigger && !innerFallback, onChange: setInnerOffset, scrollRef: popupRef, overflowRef, @@ -316,11 +316,9 @@ export namespace useSelectRoot { */ defaultValue?: any; /** - * Determines if the select should align to the selected item inside the popup or the trigger - * element. - * @default 'item' + * Determines if the selected option inside the popup should align to the trigger element. */ - alignMethod?: 'item' | 'trigger'; + alignOptionToTrigger: boolean; } export interface ReturnValue extends useFieldControlValidation.ReturnValue { diff --git a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx index f3bb27b57..2d9fca802 100644 --- a/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx +++ b/packages/mui-base/src/Select/ScrollArrow/SelectScrollArrow.tsx @@ -19,13 +19,19 @@ const SelectScrollArrow = React.forwardRef(function SelectScrollArrow( ) { const { render, className, direction, keepMounted = false, ...otherProps } = props; - const { alignMethod, innerOffset, setInnerOffset, innerFallback, popupRef, touchModality } = - useSelectRootContext(); + const { + alignOptionToTrigger, + innerOffset, + setInnerOffset, + innerFallback, + popupRef, + touchModality, + } = useSelectRootContext(); const { isPositioned, side } = useSelectPositionerContext(); const [visible, setVisible] = React.useState(false); - const inert = alignMethod === 'trigger' || touchModality; + const inert = !alignOptionToTrigger || touchModality; if (visible && inert) { setVisible(false); diff --git a/packages/mui-base/src/Test/index.tsx b/packages/mui-base/src/Test/index.tsx new file mode 100644 index 000000000..fce931ca4 --- /dev/null +++ b/packages/mui-base/src/Test/index.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +export function Root(props: Root.Props) { + return
            ; +} + +namespace Root { + export interface Props { + value?: Value; + defaultValue?: Value; + onValueChange?: (value: Value, event?: Event) => void; + } +} From aad9cb9c7d111abaabfe741211b58397a8edc681 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 2 Oct 2024 10:59:13 +1000 Subject: [PATCH 78/94] SelectAlign demo --- docs/data/components/select/SelectAlign.js | 258 ++++++++++++++++++ docs/data/components/select/SelectAlign.tsx | 258 ++++++++++++++++++ .../components/select/SelectAlign.tsx.preview | 2 + docs/data/components/select/select.mdx | 10 +- docs/next-env.d.ts | 2 +- .../Select/Positioner/useSelectPositioner.tsx | 5 +- pnpm-lock.yaml | 104 +++---- 7 files changed, 580 insertions(+), 59 deletions(-) create mode 100644 docs/data/components/select/SelectAlign.js create mode 100644 docs/data/components/select/SelectAlign.tsx create mode 100644 docs/data/components/select/SelectAlign.tsx.preview diff --git a/docs/data/components/select/SelectAlign.js b/docs/data/components/select/SelectAlign.js new file mode 100644 index 000000000..eec90f471 --- /dev/null +++ b/docs/data/components/select/SelectAlign.js @@ -0,0 +1,258 @@ +'use client'; +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; + +function AlignOptionToTriggerTrue() { + return ( + + + + + + + ( +
            +
            {props.children}
            +
            + )} + /> + + + } /> + Align option to trigger + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + ( +
            +
            {props.children}
            +
            + )} + /> +
            +
            + ); +} + +function AlignOptionToTriggerFalse() { + return ( + + + + + + + ( +
            +
            {props.children}
            +
            + )} + /> + + + } /> + Align popup to trigger + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + ( +
            +
            {props.children}
            +
            + )} + /> +
            +
            + ); +} + +export default function SelectAlign() { + return ( +
            + + +
            + ); +} + +const CheckIcon = styled(function CheckIcon(props) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + +const triggerPaddingX = 6; +const popupPadding = 4; + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: ${triggerPaddingX}px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + overflow-y: auto; + background-color: white; + padding: ${popupPadding}px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + font-size: 10px; + cursor: default; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + + > div { + top: 10px; + } + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + ${scrollArrowStyles} + bottom: 0; + + &[data-side='none'] { + bottom: -10px; + + > div { + bottom: 10px; + } + } +`; diff --git a/docs/data/components/select/SelectAlign.tsx b/docs/data/components/select/SelectAlign.tsx new file mode 100644 index 000000000..0d1071315 --- /dev/null +++ b/docs/data/components/select/SelectAlign.tsx @@ -0,0 +1,258 @@ +'use client'; +import * as React from 'react'; +import * as Select from '@base_ui/react/Select'; +import { css, styled } from '@mui/system'; + +function AlignOptionToTriggerTrue() { + return ( + + + + + + + ( +
            +
            {props.children}
            +
            + )} + /> + + + } /> + Align option to trigger + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + ( +
            +
            {props.children}
            +
            + )} + /> +
            +
            + ); +} + +function AlignOptionToTriggerFalse() { + return ( + + + + + + + ( +
            +
            {props.children}
            +
            + )} + /> + + + } /> + Align popup to trigger + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + ( +
            +
            {props.children}
            +
            + )} + /> +
            +
            + ); +} + +export default function SelectAlign() { + return ( +
            + + +
            + ); +} + +const CheckIcon = styled(function CheckIcon(props: React.SVGProps) { + return ( + + + + ); +})` + width: 100%; + height: 100%; +`; + +const triggerPaddingX = 6; +const popupPadding = 4; + +const SelectTrigger = styled(Select.Trigger)` + font-family: 'IBM Plex Sans', sans-serif; + display: flex; + align-items: center; + justify-content: space-between; + padding: ${triggerPaddingX}px 12px; + border-radius: 5px; + background-color: black; + color: white; + border: none; + font-size: 100%; + line-height: 1.5; + user-select: none; + cursor: default; + + &:focus-visible { + outline: 2px solid black; + outline-offset: 2px; + } +`; + +const SelectDropdownArrow = styled(Select.Icon)` + margin-left: 6px; + font-size: 10px; + line-height: 1; + height: 6px; +`; + +const SelectPopup = styled(Select.Popup)` + overflow-y: auto; + background-color: white; + padding: ${popupPadding}px; + border-radius: 5px; + box-shadow: + 0 2px 4px rgb(0 0 0 / 0.1), + 0 0 0 1px rgb(0 0 0 / 0.1); + max-height: var(--available-height); + outline: 0; + min-width: min( + calc(var(--available-width) - ${popupPadding * 2}px), + calc(var(--anchor-width) + ${triggerPaddingX * 2 + popupPadding * 2}px) + ); +`; + +const SelectOption = styled(Select.Option)` + padding: 6px 16px 6px 4px; + outline: 0; + cursor: default; + border-radius: 4px; + user-select: none; + display: flex; + align-items: center; + line-height: 1.5; + scroll-margin: 15px; + + &[data-disabled] { + opacity: 0.5; + } + + &[data-highlighted], + &:focus { + background-color: black; + color: white; + } +`; + +const SelectOptionIndicator = styled(Select.OptionIndicator)` + margin-right: 4px; + visibility: hidden; + width: 16px; + height: 16px; + + &[data-selected] { + visibility: visible; + } +`; + +const scrollArrowStyles = css` + width: 100%; + height: 15px; + font-size: 10px; + cursor: default; + + &[data-side='none'] { + height: 25px; + } + + > div { + position: absolute; + background: white; + width: 100%; + height: 15px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + } +`; + +const SelectScrollUpArrow = styled(Select.ScrollUpArrow)` + ${scrollArrowStyles} + + &[data-side='none'] { + top: -10px; + + > div { + top: 10px; + } + } +`; + +const SelectScrollDownArrow = styled(Select.ScrollDownArrow)` + ${scrollArrowStyles} + bottom: 0; + + &[data-side='none'] { + bottom: -10px; + + > div { + bottom: 10px; + } + } +`; diff --git a/docs/data/components/select/SelectAlign.tsx.preview b/docs/data/components/select/SelectAlign.tsx.preview new file mode 100644 index 000000000..c84013a36 --- /dev/null +++ b/docs/data/components/select/SelectAlign.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index e810b40e9..34fffe199 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -124,14 +124,14 @@ The `[data-selected]` attribute is added to the subcomponent when its owning opt ## Grouped options -`Select.OptionGroup` can be used to group options together with a label. The `Select.OptionGroupLabel` subcomponent renders the label: +`Select.Group` can be used to group options together with a label. The `Select.GroupLabel` subcomponent renders the label: ```jsx - - Label + + Label Option 1 Option 2 - + ``` @@ -147,6 +147,8 @@ By default, the selected option inside the popup is aligned to the trigger eleme - **`alignOptionToTrigger`**: aligns the popup such that the selected option inside of it appears centered over the trigger. If there's not enough space, it falls back to standard anchoring. This method is useful as it allows the user to select the an option in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS; the scroll arrow components must be used to ensure a single pointer cycle can be used. - **`alignOptionToTrigger={false}`**: aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. + + ### Scrollable popup When disabling `alignOptionToTrigger`, the select's height needs to be manually limited by its available space using CSS. diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 6f973c3e6..c4cd9dbbe 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -26,11 +26,12 @@ export function useSelectPositioner( ): useSelectPositioner.ReturnValue { const { open = false, keepMounted } = params; - const { touchModality, alignOptionToTrigger, innerFallback, mounted } = useSelectRootContext(); + const { touchModality, alignOptionToTrigger, innerFallback, mounted, triggerElement } = + useSelectRootContext(); const itemAligned = alignOptionToTrigger && !innerFallback && !touchModality; - useScrollLock(itemAligned && mounted); + useScrollLock(itemAligned && mounted, triggerElement); const { positionerStyles, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3db371a45..cf87a9295 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,7 +108,7 @@ importers: version: 7.8.0(eslint@8.57.1)(typescript@5.5.4) babel-loader: specifier: ^9.2.1 - version: 9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4)) babel-plugin-istanbul: specifier: ^6.1.1 version: 6.1.1 @@ -132,7 +132,7 @@ importers: version: 5.3.0 compression-webpack-plugin: specifier: ^11.1.0 - version: 11.1.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 11.1.0(webpack@5.91.0(webpack-cli@5.1.4)) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -144,7 +144,7 @@ importers: version: 7.0.3 css-loader: specifier: ^7.1.2 - version: 7.1.2(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 7.1.2(webpack@5.91.0(webpack-cli@5.1.4)) danger: specifier: ^12.3.3 version: 12.3.3(encoding@0.1.13) @@ -168,7 +168,7 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-import-resolver-webpack: specifier: ^0.13.9 - version: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)) eslint-plugin-babel: specifier: ^5.3.1 version: 5.3.1(eslint@8.57.1) @@ -231,7 +231,7 @@ importers: version: 0.4.0 karma-webpack: specifier: ^5.0.1 - version: 5.0.1(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 5.0.1(webpack@5.91.0(webpack-cli@5.1.4)) lerna: specifier: ^8.1.8 version: 8.1.8(babel-plugin-macros@3.1.0)(encoding@0.1.13) @@ -258,7 +258,7 @@ importers: version: 8.4.47 postcss-loader: specifier: ^8.1.1 - version: 8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4)) postcss-styled-syntax: specifier: ^0.6.4 version: 0.6.4(postcss@8.4.47) @@ -288,7 +288,7 @@ importers: version: 14.2.3 style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 4.0.0(webpack@5.91.0(webpack-cli@5.1.4)) stylelint: specifier: ^16.4.0 version: 16.4.0(typescript@5.5.4) @@ -303,7 +303,7 @@ importers: version: 5.31.0 terser-webpack-plugin: specifier: ^5.3.10 - version: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4)) tsx: specifier: ^4.8.2 version: 4.8.2 @@ -315,7 +315,7 @@ importers: version: 5.0.0 webpack: specifier: ^5.91.0 - version: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + version: 5.91.0(webpack-cli@5.1.4) webpack-bundle-analyzer: specifier: ^4.10.2 version: 4.10.2 @@ -675,7 +675,7 @@ importers: version: 11.2.0 html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + version: 5.6.0(webpack@5.91.0(webpack-cli@5.1.4)) is-wsl: specifier: ^3.1.0 version: 3.1.0 @@ -723,7 +723,7 @@ importers: version: 1.6.28 webpack: specifier: ^5.91.0 - version: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + version: 5.91.0(webpack-cli@5.1.4) yargs: specifier: ^17.7.2 version: 17.7.2 @@ -1693,11 +1693,11 @@ packages: resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@floating-ui/core@1.6.3': - resolution: {integrity: sha512-1ZpCvYf788/ZXOhRQGFxnYQOVgeU+pi0i+d0Ow34La7qjIXETi6RNswGVKkA6KcDO8/+Ysu2E/CeUmmeEBDvTg==} + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} - '@floating-ui/dom@1.6.6': - resolution: {integrity: sha512-qiTYajAnh3P+38kECeffMSQgbvXty2VB6rS+42iWR4FPIlZjLK84E9qtLnMTLIpPz2znD/TaFqaiavMUrS+Hcw==} + '@floating-ui/dom@1.6.11': + resolution: {integrity: sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==} '@floating-ui/react-dom@2.1.2': resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} @@ -9909,18 +9909,18 @@ snapshots: '@eslint/object-schema@2.1.4': {} - '@floating-ui/core@1.6.3': + '@floating-ui/core@1.6.8': dependencies: '@floating-ui/utils': 0.2.8 - '@floating-ui/dom@1.6.6': + '@floating-ui/dom@1.6.11': dependencies: - '@floating-ui/core': 1.6.3 + '@floating-ui/core': 1.6.8 '@floating-ui/utils': 0.2.8 '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@floating-ui/dom': 1.6.6 + '@floating-ui/dom': 1.6.11 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -11545,19 +11545,19 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4))': dependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4))': dependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4))': dependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0) '@xtuc/ieee754@1.2.0': {} @@ -11853,12 +11853,12 @@ snapshots: axobject-query@4.1.0: {} - babel-loader@9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + babel-loader@9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: '@babel/core': 7.25.2 find-cache-dir: 4.0.0 schema-utils: 4.2.0 - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) babel-plugin-istanbul@6.1.1: dependencies: @@ -12315,11 +12315,11 @@ snapshots: dependencies: mime-db: 1.52.0 - compression-webpack-plugin@11.1.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + compression-webpack-plugin@11.1.0(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: schema-utils: 4.2.0 serialize-javascript: 6.0.2 - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) compression@1.7.4: dependencies: @@ -12519,7 +12519,7 @@ snapshots: css-functions-list@3.2.2: {} - css-loader@7.1.2(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + css-loader@7.1.2(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: icss-utils: 5.1.0(postcss@8.4.47) postcss: 8.4.47 @@ -12530,7 +12530,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) css-select@4.3.0: dependencies: @@ -13095,7 +13095,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: debug: 3.2.7 enhanced-resolve: 0.9.1 @@ -13108,18 +13108,18 @@ snapshots: lodash: 4.17.21 resolve: 2.0.0-next.5 semver: 5.7.2 - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.8.0(eslint@8.57.1)(typescript@5.5.4) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-webpack: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + eslint-import-resolver-webpack: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)) transitivePeerDependencies: - supports-color @@ -13147,7 +13147,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -14209,7 +14209,7 @@ snapshots: readable-stream: 1.0.34 through2: 0.4.2 - html-webpack-plugin@5.6.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + html-webpack-plugin@5.6.0(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -14217,7 +14217,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) htmlparser2@6.1.0: dependencies: @@ -14887,11 +14887,11 @@ snapshots: dependencies: graceful-fs: 4.2.11 - karma-webpack@5.0.1(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + karma-webpack@5.0.1(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: glob: 7.2.3 minimatch: 9.0.4 - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) webpack-merge: 4.2.2 karma@6.4.4: @@ -16643,14 +16643,14 @@ snapshots: optionalDependencies: postcss: 8.4.47 - postcss-loader@8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + postcss-loader@8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: cosmiconfig: 9.0.0(typescript@5.5.4) jiti: 1.21.0 postcss: 8.4.47 semver: 7.6.3 optionalDependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) transitivePeerDependencies: - typescript @@ -17716,9 +17716,9 @@ snapshots: minimist: 1.2.8 through: 2.3.8 - style-loader@4.0.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + style-loader@4.0.0(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) style-to-object@0.4.4: dependencies: @@ -17917,14 +17917,14 @@ snapshots: dependencies: rimraf: 2.5.4 - terser-webpack-plugin@5.3.10(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): + terser-webpack-plugin@5.3.10(webpack@5.91.0(webpack-cli@5.1.4)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.0 - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) terser@5.31.0: dependencies: @@ -18357,9 +18357,9 @@ snapshots: webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4)) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4)) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4)) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -18368,7 +18368,7 @@ snapshots: import-local: 3.1.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) + webpack: 5.91.0(webpack-cli@5.1.4) webpack-merge: 5.10.0 optionalDependencies: webpack-bundle-analyzer: 4.10.2 @@ -18385,7 +18385,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)): + webpack@5.91.0(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.5 @@ -18408,7 +18408,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + terser-webpack-plugin: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4)) watchpack: 2.4.1 webpack-sources: 3.2.3 optionalDependencies: From b2ef0a6364aae2afa06004e1b5751ea5abfc0f18 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 2 Oct 2024 11:11:53 +1000 Subject: [PATCH 79/94] Dedupe --- pnpm-lock.yaml | 88 +++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf87a9295..aa6d965f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,7 +108,7 @@ importers: version: 7.8.0(eslint@8.57.1)(typescript@5.5.4) babel-loader: specifier: ^9.2.1 - version: 9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4)) + version: 9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) babel-plugin-istanbul: specifier: ^6.1.1 version: 6.1.1 @@ -132,7 +132,7 @@ importers: version: 5.3.0 compression-webpack-plugin: specifier: ^11.1.0 - version: 11.1.0(webpack@5.91.0(webpack-cli@5.1.4)) + version: 11.1.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -144,7 +144,7 @@ importers: version: 7.0.3 css-loader: specifier: ^7.1.2 - version: 7.1.2(webpack@5.91.0(webpack-cli@5.1.4)) + version: 7.1.2(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) danger: specifier: ^12.3.3 version: 12.3.3(encoding@0.1.13) @@ -168,7 +168,7 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-import-resolver-webpack: specifier: ^0.13.9 - version: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)) + version: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) eslint-plugin-babel: specifier: ^5.3.1 version: 5.3.1(eslint@8.57.1) @@ -231,7 +231,7 @@ importers: version: 0.4.0 karma-webpack: specifier: ^5.0.1 - version: 5.0.1(webpack@5.91.0(webpack-cli@5.1.4)) + version: 5.0.1(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) lerna: specifier: ^8.1.8 version: 8.1.8(babel-plugin-macros@3.1.0)(encoding@0.1.13) @@ -258,7 +258,7 @@ importers: version: 8.4.47 postcss-loader: specifier: ^8.1.1 - version: 8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4)) + version: 8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) postcss-styled-syntax: specifier: ^0.6.4 version: 0.6.4(postcss@8.4.47) @@ -288,7 +288,7 @@ importers: version: 14.2.3 style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.91.0(webpack-cli@5.1.4)) + version: 4.0.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) stylelint: specifier: ^16.4.0 version: 16.4.0(typescript@5.5.4) @@ -303,7 +303,7 @@ importers: version: 5.31.0 terser-webpack-plugin: specifier: ^5.3.10 - version: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4)) + version: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) tsx: specifier: ^4.8.2 version: 4.8.2 @@ -315,7 +315,7 @@ importers: version: 5.0.0 webpack: specifier: ^5.91.0 - version: 5.91.0(webpack-cli@5.1.4) + version: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) webpack-bundle-analyzer: specifier: ^4.10.2 version: 4.10.2 @@ -675,7 +675,7 @@ importers: version: 11.2.0 html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.0(webpack@5.91.0(webpack-cli@5.1.4)) + version: 5.6.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) is-wsl: specifier: ^3.1.0 version: 3.1.0 @@ -723,7 +723,7 @@ importers: version: 1.6.28 webpack: specifier: ^5.91.0 - version: 5.91.0(webpack-cli@5.1.4) + version: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) yargs: specifier: ^17.7.2 version: 17.7.2 @@ -11545,19 +11545,19 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)))': dependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)))': dependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)))': dependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0) '@xtuc/ieee754@1.2.0': {} @@ -11853,12 +11853,12 @@ snapshots: axobject-query@4.1.0: {} - babel-loader@9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4)): + babel-loader@9.2.1(@babel/core@7.25.2)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: '@babel/core': 7.25.2 find-cache-dir: 4.0.0 schema-utils: 4.2.0 - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) babel-plugin-istanbul@6.1.1: dependencies: @@ -12315,11 +12315,11 @@ snapshots: dependencies: mime-db: 1.52.0 - compression-webpack-plugin@11.1.0(webpack@5.91.0(webpack-cli@5.1.4)): + compression-webpack-plugin@11.1.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: schema-utils: 4.2.0 serialize-javascript: 6.0.2 - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) compression@1.7.4: dependencies: @@ -12519,7 +12519,7 @@ snapshots: css-functions-list@3.2.2: {} - css-loader@7.1.2(webpack@5.91.0(webpack-cli@5.1.4)): + css-loader@7.1.2(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: icss-utils: 5.1.0(postcss@8.4.47) postcss: 8.4.47 @@ -12530,7 +12530,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) css-select@4.3.0: dependencies: @@ -13095,7 +13095,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)): + eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: debug: 3.2.7 enhanced-resolve: 0.9.1 @@ -13108,18 +13108,18 @@ snapshots: lodash: 4.17.21 resolve: 2.0.0-next.5 semver: 5.7.2 - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.8.0(eslint@8.57.1)(typescript@5.5.4) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-webpack: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)) + eslint-import-resolver-webpack: 0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) transitivePeerDependencies: - supports-color @@ -13147,7 +13147,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4)))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.8.0(eslint@8.57.1)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.9(eslint-plugin-import@2.30.0)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -14209,7 +14209,7 @@ snapshots: readable-stream: 1.0.34 through2: 0.4.2 - html-webpack-plugin@5.6.0(webpack@5.91.0(webpack-cli@5.1.4)): + html-webpack-plugin@5.6.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -14217,7 +14217,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) htmlparser2@6.1.0: dependencies: @@ -14887,11 +14887,11 @@ snapshots: dependencies: graceful-fs: 4.2.11 - karma-webpack@5.0.1(webpack@5.91.0(webpack-cli@5.1.4)): + karma-webpack@5.0.1(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: glob: 7.2.3 minimatch: 9.0.4 - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) webpack-merge: 4.2.2 karma@6.4.4: @@ -16643,14 +16643,14 @@ snapshots: optionalDependencies: postcss: 8.4.47 - postcss-loader@8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4)): + postcss-loader@8.1.1(postcss@8.4.47)(typescript@5.5.4)(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: cosmiconfig: 9.0.0(typescript@5.5.4) jiti: 1.21.0 postcss: 8.4.47 semver: 7.6.3 optionalDependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) transitivePeerDependencies: - typescript @@ -17716,9 +17716,9 @@ snapshots: minimist: 1.2.8 through: 2.3.8 - style-loader@4.0.0(webpack@5.91.0(webpack-cli@5.1.4)): + style-loader@4.0.0(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) style-to-object@0.4.4: dependencies: @@ -17917,14 +17917,14 @@ snapshots: dependencies: rimraf: 2.5.4 - terser-webpack-plugin@5.3.10(webpack@5.91.0(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.10(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.0 - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) terser@5.31.0: dependencies: @@ -18357,9 +18357,9 @@ snapshots: webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -18368,7 +18368,7 @@ snapshots: import-local: 3.1.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.91.0(webpack-cli@5.1.4) + webpack: 5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)) webpack-merge: 5.10.0 optionalDependencies: webpack-bundle-analyzer: 4.10.2 @@ -18385,7 +18385,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.91.0(webpack-cli@5.1.4): + webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.5 @@ -18408,7 +18408,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(webpack@5.91.0(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.91.0))) watchpack: 2.4.1 webpack-sources: 3.2.3 optionalDependencies: From e738ac47899200c863743d8032ad5c6d57f5aa37 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 2 Oct 2024 16:51:06 +1000 Subject: [PATCH 80/94] Remove Test --- packages/mui-base/src/Test/index.tsx | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 packages/mui-base/src/Test/index.tsx diff --git a/packages/mui-base/src/Test/index.tsx b/packages/mui-base/src/Test/index.tsx deleted file mode 100644 index fce931ca4..000000000 --- a/packages/mui-base/src/Test/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; - -export function Root(props: Root.Props) { - return
            ; -} - -namespace Root { - export interface Props { - value?: Value; - defaultValue?: Value; - onValueChange?: (value: Value, event?: Event) => void; - } -} From bdbe91322f50ecc357054ea684bbdddcaa44cc77 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 2 Oct 2024 16:59:26 +1000 Subject: [PATCH 81/94] Update useButton hook usage --- packages/mui-base/src/Select/Option/useSelectOption.ts | 8 ++++---- packages/mui-base/src/Select/Trigger/useSelectTrigger.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/mui-base/src/Select/Option/useSelectOption.ts b/packages/mui-base/src/Select/Option/useSelectOption.ts index 97ee29323..267f89ec5 100644 --- a/packages/mui-base/src/Select/Option/useSelectOption.ts +++ b/packages/mui-base/src/Select/Option/useSelectOption.ts @@ -25,10 +25,10 @@ export function useSelectOption(params: useSelectOption.Parameters): useSelectOp selectionRef, } = params; - const { getRootProps: getButtonProps, rootRef: mergedRef } = useButton({ + const { getButtonProps, buttonRef } = useButton({ disabled, focusableWhenDisabled: true, - rootRef: externalRef, + buttonRef: externalRef, }); const commitSelection = useEventCallback((event: Event) => { @@ -98,9 +98,9 @@ export function useSelectOption(params: useSelectOption.Parameters): useSelectOp return React.useMemo( () => ({ getItemProps, - rootRef: mergedRef, + rootRef: buttonRef, }), - [getItemProps, mergedRef], + [getItemProps, buttonRef], ); } diff --git a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts index b3d5a6174..42e06dd6b 100644 --- a/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts +++ b/packages/mui-base/src/Select/Trigger/useSelectTrigger.ts @@ -39,13 +39,13 @@ export function useSelectTrigger( const mergedRef = useForkRef(externalRef, triggerRef); - const { getRootProps: getButtonProps, rootRef: buttonRootRef } = useButton({ + const { getButtonProps, buttonRef } = useButton({ disabled, focusableWhenDisabled: false, - rootRef: mergedRef, + buttonRef: mergedRef, }); - const handleRef = useForkRef(buttonRootRef, setTriggerElement); + const handleRef = useForkRef(buttonRef, setTriggerElement); React.useEffect(() => { if (open) { From 7f27eb3b3f98fbc172e98641be2509d87fc2fba8 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 3 Oct 2024 15:56:44 +1000 Subject: [PATCH 82/94] Feedback --- .../mui-base/src/Select/Group/SelectGroup.tsx | 1 + .../Select/GroupLabel/SelectGroupLabel.tsx | 12 +++++----- .../Select/OptionText/SelectOptionText.tsx | 11 +++++----- .../src/Select/Root/SelectRoot.test.tsx | 22 ++++--------------- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/packages/mui-base/src/Select/Group/SelectGroup.tsx b/packages/mui-base/src/Select/Group/SelectGroup.tsx index 8eced6fc2..84af210dd 100644 --- a/packages/mui-base/src/Select/Group/SelectGroup.tsx +++ b/packages/mui-base/src/Select/Group/SelectGroup.tsx @@ -71,6 +71,7 @@ namespace SelectGroup { export interface OwnerState { open: boolean; } + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} } diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx index 0d54dbd2e..848b14d75 100644 --- a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.tsx @@ -54,6 +54,12 @@ const SelectGroupLabel = React.forwardRef(function SelectGroupLabel( return renderElement(); }); +namespace SelectGroupLabel { + export interface OwnerState {} + + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} +} + SelectGroupLabel.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -77,10 +83,4 @@ SelectGroupLabel.propTypes /* remove-proptypes */ = { render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), } as any; -namespace SelectGroupLabel { - export interface OwnerState {} - - export interface Props extends BaseUIComponentProps<'div', OwnerState> {} -} - export { SelectGroupLabel }; diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index f22468b63..534741ebb 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -82,6 +82,12 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( return renderElement(); }); +namespace SelectOptionText { + export interface Props extends BaseUIComponentProps<'div', OwnerState> {} + + export interface OwnerState {} +} + SelectOptionText.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ @@ -102,8 +108,3 @@ SelectOptionText.propTypes /* remove-proptypes */ = { } as any; export { SelectOptionText }; - -namespace SelectOptionText { - export interface OwnerState {} - export interface Props extends BaseUIComponentProps<'div', OwnerState> {} -} diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index 53efa319b..d012d86c6 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -6,8 +6,6 @@ import { expect } from 'chai'; import { spy } from 'sinon'; import userEvent from '@testing-library/user-event'; -const user = userEvent.setup(); - describe('', () => { const { render } = createRenderer(); @@ -69,7 +67,7 @@ describe('', () => { }); it('should update the selected option when the value prop changes', async () => { - const { rerender } = await render( + const { setProps } = await render( @@ -94,19 +92,7 @@ describe('', () => { 'true', ); - rerender( - - - - - - - a - b - - - , - ); + setProps({ value: 'b' }); await flushMicrotasks(); @@ -146,7 +132,7 @@ describe('', () => { ); } - await render(); + const { user } = await render(); const trigger = screen.getByTestId('trigger'); @@ -214,7 +200,7 @@ describe('', () => { it('should call onOpenChange when the select is opened or closed', async () => { const handleOpenChange = spy(); - await render( + const { user } = await render( From ffc92e9145af722e2451a6fe5459134d64dba381 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 3 Oct 2024 15:57:26 +1000 Subject: [PATCH 83/94] Remove userEvent import --- packages/mui-base/src/Select/Root/SelectRoot.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index d012d86c6..7821a8231 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -4,7 +4,6 @@ import { fireEvent, flushMicrotasks, screen } from '@mui/internal-test-utils'; import { createRenderer } from '#test-utils'; import { expect } from 'chai'; import { spy } from 'sinon'; -import userEvent from '@testing-library/user-event'; describe('', () => { const { render } = createRenderer(); From 84b67b488bce524955f9d589c508756ede8f2058 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 3 Oct 2024 17:44:19 +1000 Subject: [PATCH 84/94] Handle browser autofill --- .../Select/Positioner/SelectPositioner.tsx | 43 +++++++++++-------- .../src/Select/Root/SelectRoot.test.tsx | 31 +++++++++++++ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index c86b3346a..c25114567 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -15,7 +15,6 @@ import { useFieldRootContext } from '../../Field/Root/FieldRootContext'; import { useEnhancedEffect } from '../../utils/useEnhancedEffect'; import { useId } from '../../utils/useId'; import { useLatestRef } from '../../utils/useLatestRef'; -import { mergeReactProps } from '../../utils/mergeReactProps'; import { CompositeList } from '../../Composite/List/CompositeList'; import { useField } from '../../Field/useField'; import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation'; @@ -78,6 +77,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( getInputValidationProps, touchModality, alignOptionToTrigger, + valuesRef, } = useSelectRootContext(); const { setControlId, validityData, setDirty } = useFieldRootContext(); @@ -225,22 +225,15 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( }, [value]); const mountedItemsElement = keepMounted ? null : ; + const nativeSelectElement = ( - + /> ); const shouldRender = keepMounted || mounted; diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index 7821a8231..cd3ec7131 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -224,4 +224,35 @@ describe('', () => { expect(handleOpenChange.args[1][0]).to.equal(false); }); }); + + it('should handle browser autofill', async () => { + const { container } = await render( + + + + + + + a + b + + + , + ); + + const trigger = screen.getByTestId('trigger'); + + fireEvent.click(trigger); + + await flushMicrotasks(); + + fireEvent.change(container.querySelector('[name="select"]')!, { target: { value: 'b' } }); + + await flushMicrotasks(); + + expect(screen.getByRole('option', { name: 'b', hidden: false })).to.have.attribute( + 'data-selected', + 'true', + ); + }); }); From c5716abb891a77c5b9bf89ed310e81e4a02895c1 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 3 Oct 2024 17:46:03 +1000 Subject: [PATCH 85/94] Use getInertValue --- .../mui-base/src/Select/Positioner/useSelectPositioner.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index c4cd9dbbe..66180f5c5 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -14,6 +14,7 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; import { useSelectRootContext } from '../Root/SelectRootContext'; import { useScrollLock } from '../../utils/useScrollLock'; import { MAX_Z_INDEX } from '../../utils/floating'; +import { getInertValue } from '../../utils/getInertValue'; /** * @@ -66,7 +67,7 @@ export function useSelectPositioner( return mergeReactProps(externalProps, { tabIndex: -1, - inert: open ? undefined : 'true', + inert: getInertValue(open), style: { ...positionerStyles, ...hiddenStyles, From 457a24928fbb6701ea755132c37e07577d7d72c5 Mon Sep 17 00:00:00 2001 From: atomiks Date: Thu, 3 Oct 2024 18:07:18 +1000 Subject: [PATCH 86/94] Use render user --- .../src/Select/Option/SelectOption.test.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/mui-base/src/Select/Option/SelectOption.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx index c61e24ba5..f28674d7a 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.test.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.test.tsx @@ -3,9 +3,6 @@ import * as Select from '@base_ui/react/Select'; import { fireEvent, flushMicrotasks, screen, waitFor } from '@mui/internal-test-utils'; import { createRenderer, describeConformance } from '#test-utils'; import { expect } from 'chai'; -import userEvent from '@testing-library/user-event'; - -const user = userEvent.setup(); describe('', () => { const { render } = createRenderer(); @@ -51,7 +48,7 @@ describe('', () => { }); it('navigating with keyboard should highlight option', async () => { - await render( + const { user } = await render( @@ -88,7 +85,7 @@ describe('', () => { this.skip(); } - await render( + const { user } = await render( @@ -147,7 +144,7 @@ describe('', () => { }); it('should focus the selected option upon opening the popup', async () => { - await render( + const { user } = await render( @@ -172,9 +169,9 @@ describe('', () => { expect(screen.getByRole('option', { name: 'one' })).toHaveFocus(); }); - await userEvent.keyboard('{ArrowDown}'); - await userEvent.keyboard('{ArrowUp}'); - await userEvent.keyboard('{ArrowUp}'); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowUp}'); + await user.keyboard('{ArrowUp}'); fireEvent.click(screen.getByRole('option', { name: 'three' })); fireEvent.click(trigger); @@ -188,7 +185,7 @@ describe('', () => { describe('style hooks', () => { it('should apply data-highlighted attribute when option is highlighted', async () => { - await render( + const { user } = await render( From 62d0bfdf25f38476d3803b8f49631971cb6af4b5 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 9 Oct 2024 14:15:05 +1100 Subject: [PATCH 87/94] Use generic interface --- packages/mui-base/src/Select/Root/SelectRoot.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index de0e0e86c..1f94aa8ae 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -14,7 +14,7 @@ import { useSelectRoot } from './useSelectRoot'; * * - [SelectRoot API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectRoot) */ -function SelectRoot(props: SelectRoot.Props) { +function SelectRoot(props: SelectRoot.Props): React.JSX.Element { const { animated = true, id, @@ -136,6 +136,11 @@ namespace SelectRoot { } } +interface SelectRoot { + (props: SelectRoot.Props): React.JSX.Element; + propTypes?: any; +} + SelectRoot.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ From c0d6f365c33de59b97fba34b07d1e1a53a8028a0 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 9 Oct 2024 14:18:50 +1100 Subject: [PATCH 88/94] Rename positionStrategy --- docs/data/api/select-positioner.json | 2 +- .../api-docs/select-positioner/select-positioner.json | 4 ++-- .../mui-base/src/Select/Positioner/SelectPositioner.tsx | 8 ++++---- .../src/Select/Positioner/useSelectPositioner.tsx | 6 +++--- packages/mui-base/src/Select/Root/useSelectRoot.tsx | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/data/api/select-positioner.json b/docs/data/api/select-positioner.json index b8b2f3bb7..f8ce2c199 100644 --- a/docs/data/api/select-positioner.json +++ b/docs/data/api/select-positioner.json @@ -33,7 +33,7 @@ "container": { "type": { "name": "union", "description": "HTML element
            | func" } }, "hideWhenDetached": { "type": { "name": "bool" }, "default": "false" }, "keepMounted": { "type": { "name": "bool" }, "default": "false" }, - "positionStrategy": { + "positionMethod": { "type": { "name": "enum", "description": "'absolute'
            | 'fixed'" }, "default": "'absolute'" }, diff --git a/docs/data/translations/api-docs/select-positioner/select-positioner.json b/docs/data/translations/api-docs/select-positioner/select-positioner.json index 410bc02b7..ceaa1b731 100644 --- a/docs/data/translations/api-docs/select-positioner/select-positioner.json +++ b/docs/data/translations/api-docs/select-positioner/select-positioner.json @@ -27,8 +27,8 @@ "keepMounted": { "description": "Whether the select popup remains mounted in the DOM while closed." }, - "positionStrategy": { - "description": "The CSS position strategy for positioning the Select popup element." + "positionMethod": { + "description": "The CSS position method for positioning the Select popup element." }, "render": { "description": "A function to customize rendering of the component." }, "side": { diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx index c25114567..0c511584a 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.tsx @@ -36,7 +36,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( ) { const { anchor, - positionStrategy = 'absolute', + positionMethod = 'absolute', className, render, keepMounted = false, @@ -119,7 +119,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner( const positioner = useSelectPositioner({ anchor: anchor || triggerElement, floatingRootContext, - positionStrategy, + positionMethod, container, open, mounted, @@ -395,10 +395,10 @@ SelectPositioner.propTypes /* remove-proptypes */ = { */ keepMounted: PropTypes.bool, /** - * The CSS position strategy for positioning the Select popup element. + * The CSS position method for positioning the Select popup element. * @default 'absolute' */ - positionStrategy: PropTypes.oneOf(['absolute', 'fixed']), + positionMethod: PropTypes.oneOf(['absolute', 'fixed']), /** * A function to customize rendering of the component. */ diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 66180f5c5..73a52e983 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -46,7 +46,7 @@ export function useSelectPositioner( isPositioned, } = useAnchorPositioning({ ...params, - positionStrategy: itemAligned ? 'fixed' : params.positionStrategy, + positionMethod: itemAligned ? 'fixed' : params.positionMethod, innerOptions: { fallback: params.innerFallback, touchModality, @@ -116,10 +116,10 @@ export namespace useSelectPositioner { | React.MutableRefObject | (() => Element | VirtualElement | null); /** - * The CSS position strategy for positioning the Select popup element. + * The CSS position method for positioning the Select popup element. * @default 'absolute' */ - positionStrategy?: 'absolute' | 'fixed'; + positionMethod?: 'absolute' | 'fixed'; /** * The container element to which the Select popup will be appended to. */ diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 91bfedcca..68df0534b 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -45,7 +45,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R alignOptionToTrigger, } = params; - const { setDirty, validityData, validateOnChange } = useFieldRootContext(); + const { setDirty, validityData, validationMode } = useFieldRootContext(); const fieldControlValidation = useFieldControlValidation(); const [triggerElement, setTriggerElement] = React.useState(null); @@ -87,7 +87,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R setDirty(nextValue !== validityData.initialValue); - if (validateOnChange) { + if (validationMode === 'onChange') { fieldControlValidation.commitValidation(nextValue); } From f388d06334c0927ef43fdbb48edf2f82de22671b Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 9 Oct 2024 14:47:15 +1100 Subject: [PATCH 89/94] Type SelectRoot --- packages/mui-base/src/Select/Root/SelectRoot.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/mui-base/src/Select/Root/SelectRoot.tsx b/packages/mui-base/src/Select/Root/SelectRoot.tsx index 1f94aa8ae..54932fdee 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.tsx @@ -14,7 +14,9 @@ import { useSelectRoot } from './useSelectRoot'; * * - [SelectRoot API](https://base-ui.netlify.app/components/react-select/#api-reference-SelectRoot) */ -function SelectRoot(props: SelectRoot.Props): React.JSX.Element { +const SelectRoot: SelectRoot = function SelectRoot( + props: SelectRoot.Props, +): React.JSX.Element { const { animated = true, id, @@ -60,10 +62,10 @@ function SelectRoot(props: SelectRoot.Props): React.JSX.Element { ); return {children}; -} +}; namespace SelectRoot { - export interface Props { + export interface Props { /** * If `true`, the Select supports CSS-based animations and transitions. * It is kept in the DOM until the animation completes. From f83840e8c0787bb2f33cf65d4b9936f0e167977c Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 9 Oct 2024 14:53:38 +1100 Subject: [PATCH 90/94] Invert inert --- packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 73a52e983..6362eba3c 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -67,7 +67,7 @@ export function useSelectPositioner( return mergeReactProps(externalProps, { tabIndex: -1, - inert: getInertValue(open), + inert: getInertValue(!open), style: { ...positionerStyles, ...hiddenStyles, From 93c64a1d487a1bdd48032595ae45a89880562ce2 Mon Sep 17 00:00:00 2001 From: atomiks Date: Fri, 11 Oct 2024 16:03:40 +1100 Subject: [PATCH 91/94] Fix object value warning --- packages/mui-base/src/Select/Root/useSelectRoot.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index 68df0534b..b5231a057 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -99,14 +99,13 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R useEnhancedEffect(() => { // Wait for the items to have registered their values in `valuesRef`. queueMicrotask(() => { - const index = valuesRef.current.indexOf(value); + const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + const index = valuesRef.current.indexOf(stringValue); if (index !== -1) { setSelectedIndex(index); setLabel(labelsRef.current[index] ?? ''); } else if (value) { - warn( - `The value \`${typeof value === 'string' ? value : JSON.stringify(value)}\` is not present in the Select options.`, - ); + warn(`The value \`${stringValue}\` is not present in the Select options.`); } }); }, [value]); From 1ef2828989f230fc4938a7b79930773a9b7b854d Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 14 Oct 2024 13:42:23 +1100 Subject: [PATCH 92/94] Handle null default value --- packages/mui-base/src/Select/Root/useSelectRoot.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mui-base/src/Select/Root/useSelectRoot.tsx b/packages/mui-base/src/Select/Root/useSelectRoot.tsx index b5231a057..b64808ce0 100644 --- a/packages/mui-base/src/Select/Root/useSelectRoot.tsx +++ b/packages/mui-base/src/Select/Root/useSelectRoot.tsx @@ -99,7 +99,8 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelectRoot.R useEnhancedEffect(() => { // Wait for the items to have registered their values in `valuesRef`. queueMicrotask(() => { - const stringValue = typeof value === 'string' ? value : JSON.stringify(value); + const stringValue = + typeof value === 'string' || value === null ? value : JSON.stringify(value); const index = valuesRef.current.indexOf(stringValue); if (index !== -1) { setSelectedIndex(index); From cccd3b2c75c3d133283e8bb5d0257766cc833978 Mon Sep 17 00:00:00 2001 From: atomiks Date: Mon, 14 Oct 2024 20:09:30 +1100 Subject: [PATCH 93/94] Migrate new exports --- docs/data/api/select-arrow.json | 4 +--- docs/data/api/select-backdrop.json | 2 +- docs/data/api/select-group-label.json | 2 +- docs/data/api/select-group.json | 4 +--- docs/data/api/select-icon.json | 2 +- docs/data/api/select-option-group-label.json | 2 +- docs/data/api/select-option-group.json | 2 +- docs/data/api/select-option-indicator.json | 2 +- docs/data/api/select-option-text.json | 2 +- docs/data/api/select-option.json | 2 +- docs/data/api/select-popup.json | 4 +--- docs/data/api/select-positioner.json | 2 +- docs/data/api/select-root.json | 2 +- docs/data/api/select-scroll-down-arrow.json | 2 +- docs/data/api/select-scroll-up-arrow.json | 2 +- docs/data/api/select-separator.json | 2 +- docs/data/api/select-trigger.json | 2 +- docs/data/api/select-value.json | 4 +--- docs/data/components/select/SelectAlign.js | 2 +- docs/data/components/select/SelectAlign.tsx | 2 +- docs/data/components/select/SelectEmpty.js | 2 +- docs/data/components/select/SelectEmpty.tsx | 2 +- docs/data/components/select/SelectGroup.js | 2 +- docs/data/components/select/SelectGroup.tsx | 2 +- .../select/SelectIntroduction/system/index.js | 2 +- .../select/SelectIntroduction/system/index.tsx | 2 +- .../mui-base/src/Field/Root/FieldRoot.test.tsx | 2 +- .../src/Select/Arrow/SelectArrow.test.tsx | 2 +- .../mui-base/src/Select/Arrow/SelectArrow.tsx | 1 + .../src/Select/Backdrop/SelectBackdrop.test.tsx | 2 +- .../src/Select/Backdrop/SelectBackdrop.tsx | 1 + .../src/Select/Group/SelectGroup.test.tsx | 2 +- .../Select/GroupLabel/SelectGroupLabel.test.tsx | 2 +- .../src/Select/Icon/SelectIcon.test.tsx | 2 +- .../src/Select/Option/SelectOption.test.tsx | 2 +- .../SelectOptionIndicator.test.tsx | 2 +- .../OptionIndicator/SelectOptionIndicator.tsx | 1 + .../Select/OptionText/SelectOptionText.test.tsx | 2 +- .../src/Select/Popup/SelectPopup.test.tsx | 2 +- .../Select/Positioner/SelectPositioner.test.tsx | 2 +- .../src/Select/Root/SelectRoot.test.tsx | 2 +- .../SelectScrollDownArrow.test.tsx | 2 +- .../ScrollUpArrow/SelectScrollUpArrow.test.tsx | 2 +- .../src/Select/Trigger/SelectTrigger.test.tsx | 2 +- .../src/Select/Value/SelectValue.test.tsx | 2 +- packages/mui-base/src/Select/index.barrel.ts | 16 ---------------- packages/mui-base/src/Select/index.parts.ts | 16 ++++++++++++++++ packages/mui-base/src/Select/index.ts | 17 +---------------- packages/mui-base/src/index.ts | 4 ++-- 49 files changed, 64 insertions(+), 84 deletions(-) delete mode 100644 packages/mui-base/src/Select/index.barrel.ts create mode 100644 packages/mui-base/src/Select/index.parts.ts diff --git a/docs/data/api/select-arrow.json b/docs/data/api/select-arrow.json index 9d3f1a6d7..24eed4806 100644 --- a/docs/data/api/select-arrow.json +++ b/docs/data/api/select-arrow.json @@ -5,9 +5,7 @@ "render": { "type": { "name": "union", "description": "element
            | func" } } }, "name": "SelectArrow", - "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectArrow = Select.Arrow;" - ], + "imports": ["import { Select } from '@base_ui/react/Select';\nconst SelectArrow = Select.Arrow;"], "classes": [], "spread": true, "themeDefaultProps": true, diff --git a/docs/data/api/select-backdrop.json b/docs/data/api/select-backdrop.json index 2138d5222..fd014aaa7 100644 --- a/docs/data/api/select-backdrop.json +++ b/docs/data/api/select-backdrop.json @@ -10,7 +10,7 @@ }, "name": "SelectBackdrop", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectBackdrop = Select.Backdrop;" + "import { Select } from '@base_ui/react/Select';\nconst SelectBackdrop = Select.Backdrop;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-group-label.json b/docs/data/api/select-group-label.json index fc820da7a..2ad62bed8 100644 --- a/docs/data/api/select-group-label.json +++ b/docs/data/api/select-group-label.json @@ -5,7 +5,7 @@ }, "name": "SelectGroupLabel", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectGroupLabel = Select.GroupLabel;" + "import { Select } from '@base_ui/react/Select';\nconst SelectGroupLabel = Select.GroupLabel;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-group.json b/docs/data/api/select-group.json index 9895f3044..b820a0393 100644 --- a/docs/data/api/select-group.json +++ b/docs/data/api/select-group.json @@ -4,9 +4,7 @@ "render": { "type": { "name": "union", "description": "element
            | func" } } }, "name": "SelectGroup", - "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectGroup = Select.Group;" - ], + "imports": ["import { Select } from '@base_ui/react/Select';\nconst SelectGroup = Select.Group;"], "classes": [], "spread": true, "themeDefaultProps": true, diff --git a/docs/data/api/select-icon.json b/docs/data/api/select-icon.json index 9ddfe9fdb..e57e71336 100644 --- a/docs/data/api/select-icon.json +++ b/docs/data/api/select-icon.json @@ -4,7 +4,7 @@ "render": { "type": { "name": "union", "description": "element
            | func" } } }, "name": "SelectIcon", - "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectIcon = Select.Icon;"], + "imports": ["import { Select } from '@base_ui/react/Select';\nconst SelectIcon = Select.Icon;"], "classes": [], "spread": true, "themeDefaultProps": true, diff --git a/docs/data/api/select-option-group-label.json b/docs/data/api/select-option-group-label.json index d975856fb..94298762e 100644 --- a/docs/data/api/select-option-group-label.json +++ b/docs/data/api/select-option-group-label.json @@ -5,7 +5,7 @@ }, "name": "SelectOptionGroupLabel", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectOptionGroupLabel = Select.OptionGroupLabel;" + "import { Select } from '@base_ui/react/Select';\nconst SelectOptionGroupLabel = Select.OptionGroupLabel;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-option-group.json b/docs/data/api/select-option-group.json index 81b819ef9..222bd099f 100644 --- a/docs/data/api/select-option-group.json +++ b/docs/data/api/select-option-group.json @@ -5,7 +5,7 @@ }, "name": "SelectOptionGroup", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectOptionGroup = Select.OptionGroup;" + "import { Select } from '@base_ui/react/Select';\nconst SelectOptionGroup = Select.OptionGroup;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-option-indicator.json b/docs/data/api/select-option-indicator.json index 1dbd62192..f5eb01fb3 100644 --- a/docs/data/api/select-option-indicator.json +++ b/docs/data/api/select-option-indicator.json @@ -6,7 +6,7 @@ }, "name": "SelectOptionIndicator", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectOptionIndicator = Select.OptionIndicator;" + "import { Select } from '@base_ui/react/Select';\nconst SelectOptionIndicator = Select.OptionIndicator;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-option-text.json b/docs/data/api/select-option-text.json index ec5d5b486..07042ca8c 100644 --- a/docs/data/api/select-option-text.json +++ b/docs/data/api/select-option-text.json @@ -5,7 +5,7 @@ }, "name": "SelectOptionText", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectOptionText = Select.OptionText;" + "import { Select } from '@base_ui/react/Select';\nconst SelectOptionText = Select.OptionText;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-option.json b/docs/data/api/select-option.json index 7bc4547e8..9457c6d29 100644 --- a/docs/data/api/select-option.json +++ b/docs/data/api/select-option.json @@ -7,7 +7,7 @@ }, "name": "SelectOption", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectOption = Select.Option;" + "import { Select } from '@base_ui/react/Select';\nconst SelectOption = Select.Option;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-popup.json b/docs/data/api/select-popup.json index 309e0d76c..3f593a177 100644 --- a/docs/data/api/select-popup.json +++ b/docs/data/api/select-popup.json @@ -5,9 +5,7 @@ "render": { "type": { "name": "union", "description": "element
            | func" } } }, "name": "SelectPopup", - "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectPopup = Select.Popup;" - ], + "imports": ["import { Select } from '@base_ui/react/Select';\nconst SelectPopup = Select.Popup;"], "classes": [], "spread": true, "themeDefaultProps": true, diff --git a/docs/data/api/select-positioner.json b/docs/data/api/select-positioner.json index f8ce2c199..a2bfd3507 100644 --- a/docs/data/api/select-positioner.json +++ b/docs/data/api/select-positioner.json @@ -50,7 +50,7 @@ }, "name": "SelectPositioner", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectPositioner = Select.Positioner;" + "import { Select } from '@base_ui/react/Select';\nconst SelectPositioner = Select.Positioner;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-root.json b/docs/data/api/select-root.json index 8eb3aad7a..75cf8d4c3 100644 --- a/docs/data/api/select-root.json +++ b/docs/data/api/select-root.json @@ -16,7 +16,7 @@ "value": { "type": { "name": "any" } } }, "name": "SelectRoot", - "imports": ["import * as Select from '@base_ui/react/Select';\nconst SelectRoot = Select.Root;"], + "imports": ["import { Select } from '@base_ui/react/Select';\nconst SelectRoot = Select.Root;"], "classes": [], "spread": true, "themeDefaultProps": null, diff --git a/docs/data/api/select-scroll-down-arrow.json b/docs/data/api/select-scroll-down-arrow.json index 18e40893d..af08fc9a4 100644 --- a/docs/data/api/select-scroll-down-arrow.json +++ b/docs/data/api/select-scroll-down-arrow.json @@ -2,7 +2,7 @@ "props": { "keepMounted": { "type": { "name": "bool" }, "default": "false" } }, "name": "SelectScrollDownArrow", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectScrollDownArrow = Select.ScrollDownArrow;" + "import { Select } from '@base_ui/react/Select';\nconst SelectScrollDownArrow = Select.ScrollDownArrow;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-scroll-up-arrow.json b/docs/data/api/select-scroll-up-arrow.json index d0caea010..140b5c8df 100644 --- a/docs/data/api/select-scroll-up-arrow.json +++ b/docs/data/api/select-scroll-up-arrow.json @@ -2,7 +2,7 @@ "props": { "keepMounted": { "type": { "name": "bool" }, "default": "false" } }, "name": "SelectScrollUpArrow", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectScrollUpArrow = Select.ScrollUpArrow;" + "import { Select } from '@base_ui/react/Select';\nconst SelectScrollUpArrow = Select.ScrollUpArrow;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-separator.json b/docs/data/api/select-separator.json index 2f691f9d6..0c2085128 100644 --- a/docs/data/api/select-separator.json +++ b/docs/data/api/select-separator.json @@ -5,7 +5,7 @@ }, "name": "SelectSeparator", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectSeparator = Select.Separator;" + "import { Select } from '@base_ui/react/Select';\nconst SelectSeparator = Select.Separator;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-trigger.json b/docs/data/api/select-trigger.json index 16364da74..f816e4ed5 100644 --- a/docs/data/api/select-trigger.json +++ b/docs/data/api/select-trigger.json @@ -8,7 +8,7 @@ }, "name": "SelectTrigger", "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectTrigger = Select.Trigger;" + "import { Select } from '@base_ui/react/Select';\nconst SelectTrigger = Select.Trigger;" ], "classes": [], "spread": true, diff --git a/docs/data/api/select-value.json b/docs/data/api/select-value.json index 174fb3be2..2a7d405db 100644 --- a/docs/data/api/select-value.json +++ b/docs/data/api/select-value.json @@ -5,9 +5,7 @@ "render": { "type": { "name": "union", "description": "element
            | func" } } }, "name": "SelectValue", - "imports": [ - "import * as Select from '@base_ui/react/Select';\nconst SelectValue = Select.Value;" - ], + "imports": ["import { Select } from '@base_ui/react/Select';\nconst SelectValue = Select.Value;"], "classes": [], "spread": true, "themeDefaultProps": true, diff --git a/docs/data/components/select/SelectAlign.js b/docs/data/components/select/SelectAlign.js index eec90f471..4bbc0bf80 100644 --- a/docs/data/components/select/SelectAlign.js +++ b/docs/data/components/select/SelectAlign.js @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; function AlignOptionToTriggerTrue() { diff --git a/docs/data/components/select/SelectAlign.tsx b/docs/data/components/select/SelectAlign.tsx index 0d1071315..4c0d8041f 100644 --- a/docs/data/components/select/SelectAlign.tsx +++ b/docs/data/components/select/SelectAlign.tsx @@ -1,6 +1,6 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; function AlignOptionToTriggerTrue() { diff --git a/docs/data/components/select/SelectEmpty.js b/docs/data/components/select/SelectEmpty.js index cc3a018cf..5a4fc6049 100644 --- a/docs/data/components/select/SelectEmpty.js +++ b/docs/data/components/select/SelectEmpty.js @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; export default function SelectEmpty() { diff --git a/docs/data/components/select/SelectEmpty.tsx b/docs/data/components/select/SelectEmpty.tsx index a7c51747c..9774f0587 100644 --- a/docs/data/components/select/SelectEmpty.tsx +++ b/docs/data/components/select/SelectEmpty.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; export default function SelectEmpty() { diff --git a/docs/data/components/select/SelectGroup.js b/docs/data/components/select/SelectGroup.js index d512baa00..88d50360e 100644 --- a/docs/data/components/select/SelectGroup.js +++ b/docs/data/components/select/SelectGroup.js @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; function createOptions(items) { diff --git a/docs/data/components/select/SelectGroup.tsx b/docs/data/components/select/SelectGroup.tsx index 405bfd129..6deffbdff 100644 --- a/docs/data/components/select/SelectGroup.tsx +++ b/docs/data/components/select/SelectGroup.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; function createOptions(items: string[]) { diff --git a/docs/data/components/select/SelectIntroduction/system/index.js b/docs/data/components/select/SelectIntroduction/system/index.js index 58c72be26..a023af86c 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.js +++ b/docs/data/components/select/SelectIntroduction/system/index.js @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; export default function SelectIntroduction() { diff --git a/docs/data/components/select/SelectIntroduction/system/index.tsx b/docs/data/components/select/SelectIntroduction/system/index.tsx index 7137afae7..3585165ba 100644 --- a/docs/data/components/select/SelectIntroduction/system/index.tsx +++ b/docs/data/components/select/SelectIntroduction/system/index.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { css, styled } from '@mui/system'; export default function SelectIntroduction() { diff --git a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx index 13dc641fd..ea6cf99dc 100644 --- a/packages/mui-base/src/Field/Root/FieldRoot.test.tsx +++ b/packages/mui-base/src/Field/Root/FieldRoot.test.tsx @@ -6,7 +6,7 @@ import { NumberField } from '@base_ui/react/NumberField'; import { Slider } from '@base_ui/react/Slider'; import { RadioGroup } from '@base_ui/react/RadioGroup'; import { Radio } from '@base_ui/react/Radio'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import userEvent from '@testing-library/user-event'; import { act, fireEvent, flushMicrotasks, screen, waitFor } from '@mui/internal-test-utils'; import { expect } from 'chai'; diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx index 54de5be14..e1da70572 100644 --- a/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx index 4602a5373..54544bb3c 100644 --- a/packages/mui-base/src/Select/Arrow/SelectArrow.tsx +++ b/packages/mui-base/src/Select/Arrow/SelectArrow.tsx @@ -8,6 +8,7 @@ import { useForkRef } from '../../utils/useForkRef'; import { mergeReactProps } from '../../utils/mergeReactProps'; import type { BaseUIComponentProps } from '../../utils/types'; import { commonStyleHooks } from '../utils/commonStyleHooks'; + /** * * Demos: diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx index 75e9c32e0..3d8251c77 100644 --- a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx index 93296ff7d..1af59f6ea 100644 --- a/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx +++ b/packages/mui-base/src/Select/Backdrop/SelectBackdrop.tsx @@ -12,6 +12,7 @@ import { commonStyleHooks } from '../utils/commonStyleHooks'; import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; const customStyleHookMapping: CustomStyleHookMapping = commonStyleHooks; + /** * * Demos: diff --git a/packages/mui-base/src/Select/Group/SelectGroup.test.tsx b/packages/mui-base/src/Select/Group/SelectGroup.test.tsx index 4d7ddbf4c..a9a673870 100644 --- a/packages/mui-base/src/Select/Group/SelectGroup.test.tsx +++ b/packages/mui-base/src/Select/Group/SelectGroup.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; import { screen } from '@mui/internal-test-utils'; import { expect } from 'chai'; diff --git a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx index 7e7c0c4a8..85f838786 100644 --- a/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx +++ b/packages/mui-base/src/Select/GroupLabel/SelectGroupLabel.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx b/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx index 8190313ed..1ab6b6f44 100644 --- a/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx +++ b/packages/mui-base/src/Select/Icon/SelectIcon.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Option/SelectOption.test.tsx b/packages/mui-base/src/Select/Option/SelectOption.test.tsx index f28674d7a..b91100e4d 100644 --- a/packages/mui-base/src/Select/Option/SelectOption.test.tsx +++ b/packages/mui-base/src/Select/Option/SelectOption.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { fireEvent, flushMicrotasks, screen, waitFor } from '@mui/internal-test-utils'; import { createRenderer, describeConformance } from '#test-utils'; import { expect } from 'chai'; diff --git a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx index 46d64b47f..a7eea01a3 100644 --- a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; import { SelectOptionContext } from '../Option/SelectOptionContext'; diff --git a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx index bb0390b84..08ca08ab0 100644 --- a/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx +++ b/packages/mui-base/src/Select/OptionIndicator/SelectOptionIndicator.tsx @@ -11,6 +11,7 @@ import { mergeReactProps } from '../../utils/mergeReactProps'; const customStyleHookMapping: CustomStyleHookMapping = commonStyleHooks; + /** * * Demos: diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx index 87f30a38e..34d5e12be 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx b/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx index 14ca06177..c3a0002c3 100644 --- a/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx +++ b/packages/mui-base/src/Select/Popup/SelectPopup.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx b/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx index 301f38df2..362d51288 100644 --- a/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx +++ b/packages/mui-base/src/Select/Positioner/SelectPositioner.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx index cd3ec7131..e9d04fe51 100644 --- a/packages/mui-base/src/Select/Root/SelectRoot.test.tsx +++ b/packages/mui-base/src/Select/Root/SelectRoot.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { fireEvent, flushMicrotasks, screen } from '@mui/internal-test-utils'; import { createRenderer } from '#test-utils'; import { expect } from 'chai'; diff --git a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx index 2638a763b..4caea6b87 100644 --- a/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx +++ b/packages/mui-base/src/Select/ScrollDownArrow/SelectScrollDownArrow.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx index 7eae77b05..37b6670a8 100644 --- a/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx +++ b/packages/mui-base/src/Select/ScrollUpArrow/SelectScrollUpArrow.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx b/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx index 763f785e7..aed44c759 100644 --- a/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx +++ b/packages/mui-base/src/Select/Trigger/SelectTrigger.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { createRenderer, describeConformance } from '#test-utils'; describe('', () => { diff --git a/packages/mui-base/src/Select/Value/SelectValue.test.tsx b/packages/mui-base/src/Select/Value/SelectValue.test.tsx index 6bd665549..55fe19457 100644 --- a/packages/mui-base/src/Select/Value/SelectValue.test.tsx +++ b/packages/mui-base/src/Select/Value/SelectValue.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as Select from '@base_ui/react/Select'; +import { Select } from '@base_ui/react/Select'; import { screen } from '@mui/internal-test-utils'; import { createRenderer, describeConformance } from '#test-utils'; import { expect } from 'chai'; diff --git a/packages/mui-base/src/Select/index.barrel.ts b/packages/mui-base/src/Select/index.barrel.ts deleted file mode 100644 index 0e1b00dcb..000000000 --- a/packages/mui-base/src/Select/index.barrel.ts +++ /dev/null @@ -1,16 +0,0 @@ -export { SelectRoot } from './Root/SelectRoot'; -export { SelectTrigger } from './Trigger/SelectTrigger'; -export { SelectPositioner } from './Positioner/SelectPositioner'; -export { SelectPopup } from './Popup/SelectPopup'; -export { SelectBackdrop } from './Backdrop/SelectBackdrop'; -export { SelectOption } from './Option/SelectOption'; -export { SelectOptionIndicator } from './OptionIndicator/SelectOptionIndicator'; -export { SelectGroup } from './Group/SelectGroup'; -export { SelectGroupLabel } from './GroupLabel/SelectGroupLabel'; -export { SelectValue } from './Value/SelectValue'; -export { SelectScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; -export { SelectScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; -export { SeparatorRoot as SelectSeparator } from '../Separator/Root/SeparatorRoot'; -export { SelectIcon } from './Icon/SelectIcon'; -export { SelectArrow } from './Arrow/SelectArrow'; -export { SelectOptionText } from './OptionText/SelectOptionText'; diff --git a/packages/mui-base/src/Select/index.parts.ts b/packages/mui-base/src/Select/index.parts.ts new file mode 100644 index 000000000..56779ec33 --- /dev/null +++ b/packages/mui-base/src/Select/index.parts.ts @@ -0,0 +1,16 @@ +export { SelectRoot as Root } from './Root/SelectRoot'; +export { SelectTrigger as Trigger } from './Trigger/SelectTrigger'; +export { SelectPositioner as Positioner } from './Positioner/SelectPositioner'; +export { SelectPopup as Popup } from './Popup/SelectPopup'; +export { SelectBackdrop as Backdrop } from './Backdrop/SelectBackdrop'; +export { SelectOption as Option } from './Option/SelectOption'; +export { SelectOptionIndicator as OptionIndicator } from './OptionIndicator/SelectOptionIndicator'; +export { SelectGroup as Group } from './Group/SelectGroup'; +export { SelectGroupLabel as GroupLabel } from './GroupLabel/SelectGroupLabel'; +export { SelectValue as Value } from './Value/SelectValue'; +export { SelectScrollUpArrow as ScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; +export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; +export { SeparatorRoot as Separator } from '../Separator/Root/SeparatorRoot'; +export { SelectIcon as Icon } from './Icon/SelectIcon'; +export { SelectArrow as Arrow } from './Arrow/SelectArrow'; +export { SelectOptionText as OptionText } from './OptionText/SelectOptionText'; diff --git a/packages/mui-base/src/Select/index.ts b/packages/mui-base/src/Select/index.ts index 56779ec33..189ac83bf 100644 --- a/packages/mui-base/src/Select/index.ts +++ b/packages/mui-base/src/Select/index.ts @@ -1,16 +1 @@ -export { SelectRoot as Root } from './Root/SelectRoot'; -export { SelectTrigger as Trigger } from './Trigger/SelectTrigger'; -export { SelectPositioner as Positioner } from './Positioner/SelectPositioner'; -export { SelectPopup as Popup } from './Popup/SelectPopup'; -export { SelectBackdrop as Backdrop } from './Backdrop/SelectBackdrop'; -export { SelectOption as Option } from './Option/SelectOption'; -export { SelectOptionIndicator as OptionIndicator } from './OptionIndicator/SelectOptionIndicator'; -export { SelectGroup as Group } from './Group/SelectGroup'; -export { SelectGroupLabel as GroupLabel } from './GroupLabel/SelectGroupLabel'; -export { SelectValue as Value } from './Value/SelectValue'; -export { SelectScrollUpArrow as ScrollUpArrow } from './ScrollUpArrow/SelectScrollUpArrow'; -export { SelectScrollDownArrow as ScrollDownArrow } from './ScrollDownArrow/SelectScrollDownArrow'; -export { SeparatorRoot as Separator } from '../Separator/Root/SeparatorRoot'; -export { SelectIcon as Icon } from './Icon/SelectIcon'; -export { SelectArrow as Arrow } from './Arrow/SelectArrow'; -export { SelectOptionText as OptionText } from './OptionText/SelectOptionText'; +export * as Select from './index.parts'; diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts index abb83195b..28bb27e96 100644 --- a/packages/mui-base/src/index.ts +++ b/packages/mui-base/src/index.ts @@ -7,7 +7,7 @@ export * from './Field'; export * from './Fieldset'; export * from './Form'; export * from './Menu'; -export * from './NumberField'; +export * from './Select'; export * from './Popover'; export * from './PreviewCard'; export * from './Progress'; @@ -17,4 +17,4 @@ export * from './Select'; export * from './Slider'; export * from './Switch'; export * from './Tabs'; -export * from './Tooltip'; \ No newline at end of file +export * from './Tooltip'; From 730f6f290b6d20a7b473d3b9c4f5c6759b41b49c Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 15 Oct 2024 15:13:23 +1100 Subject: [PATCH 94/94] Adjust positioning --- docs/data/components/select/select.mdx | 6 ++++-- .../src/Select/OptionText/SelectOptionText.tsx | 13 +++++++++++-- .../src/Select/Positioner/useSelectPositioner.tsx | 2 -- packages/mui-base/src/utils/useAnchorPositioning.ts | 10 ++-------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/data/components/select/select.mdx b/docs/data/components/select/select.mdx index 34fffe199..3fb2aa882 100644 --- a/docs/data/components/select/select.mdx +++ b/docs/data/components/select/select.mdx @@ -144,14 +144,16 @@ By default, the selected option inside the popup is aligned to the trigger eleme ``` -- **`alignOptionToTrigger`**: aligns the popup such that the selected option inside of it appears centered over the trigger. If there's not enough space, it falls back to standard anchoring. This method is useful as it allows the user to select the an option in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS; the scroll arrow components must be used to ensure a single pointer cycle can be used. +- **`alignOptionToTrigger={true}`**: aligns the popup such that the selected option inside of it appears centered over the trigger. If there's not enough space, it falls back to standard anchoring. This method is useful as it allows the user to select an option in a single click or "pointer cycle" (pointer down, pointer move, pointer up). This is the native behavior on macOS; the scroll arrow components must be used to ensure a single pointer cycle can be used. - **`alignOptionToTrigger={false}`**: aligns the popup to the trigger itself on its top or bottom side, which is the standard form of anchor positioning used in Tooltip, Popover, Menu, etc. +This option is always `false` on touch devices or touch input. + ### Scrollable popup -When disabling `alignOptionToTrigger`, the select's height needs to be manually limited by its available space using CSS. +The select's height needs to be manually limited by its available space using CSS. This can be achieved by using the `--available-height` CSS variable: diff --git a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx index 534741ebb..48d4c6a38 100644 --- a/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx +++ b/packages/mui-base/src/Select/OptionText/SelectOptionText.tsx @@ -25,8 +25,15 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( ) { const { className, render, ...otherProps } = props; - const { open, triggerElement, valueRef, popupRef, innerFallback, alignOptionToTrigger } = - useSelectRootContext(); + const { + open, + triggerElement, + valueRef, + popupRef, + innerFallback, + touchModality, + alignOptionToTrigger, + } = useSelectRootContext(); const { isPositioned, setOptionTextOffset } = useSelectPositionerContext(); const { selected } = useSelectOptionContext(); @@ -38,6 +45,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( useEnhancedEffect(() => { if ( !alignOptionToTrigger || + touchModality || innerFallback || !open || !isPositioned || @@ -69,6 +77,7 @@ const SelectOptionText = React.forwardRef(function SelectOptionText( setOptionTextOffset, triggerElement, valueRef, + touchModality, ]); const { renderElement } = useComponentRenderer({ diff --git a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx index 6362eba3c..8f661d902 100644 --- a/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx +++ b/packages/mui-base/src/Select/Positioner/useSelectPositioner.tsx @@ -52,8 +52,6 @@ export function useSelectPositioner( touchModality, }, trackAnchor: !itemAligned, - collisionPadding: - touchModality && params.collisionPadding == null ? 20 : params.collisionPadding, }); const getPositionerProps: useSelectPositioner.ReturnValue['getPositionerProps'] = diff --git a/packages/mui-base/src/utils/useAnchorPositioning.ts b/packages/mui-base/src/utils/useAnchorPositioning.ts index 3b9abbcea..918dddffe 100644 --- a/packages/mui-base/src/utils/useAnchorPositioning.ts +++ b/packages/mui-base/src/utils/useAnchorPositioning.ts @@ -102,7 +102,7 @@ export function useAnchorPositioning( innerOptions = {}, } = params; - const standardMode = !(!innerOptions.fallback && innerMiddleware); + const standardMode = innerOptions.touchModality || !(!innerOptions.fallback && innerMiddleware); const placement = alignment === 'center' ? side : (`${side}-${alignment}` as Placement); const commonCollisionProps = { @@ -153,13 +153,7 @@ export function useAnchorPositioning( } middleware.push( - ...(!standardMode - ? [innerMiddleware, shiftMiddleware] - : [ - innerOptions.touchModality - ? shift({ crossAxis: true, ...commonCollisionProps }) - : (false as const), - ]), + ...(!standardMode ? [innerMiddleware, shiftMiddleware] : []), size({ ...commonCollisionProps, apply({ elements: { floating }, rects: { reference }, availableWidth, availableHeight }) {