Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks committed Aug 23, 2024
1 parent cd8f086 commit 7bed7cc
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import Check from '@mui/icons-material/Check';

export default function UnstyledSelectIntroduction() {
return (
<Select.Root>
<Select.Root value="item-2">
<SelectTrigger>Trigger</SelectTrigger>
<Select.Backdrop />
<Select.Positioner alignment="start" alignmentOffset={-4}>
<SelectPopup>
{[...Array(100)].map((_, index) => (
<SelectItem key={index}>
<SelectItem key={index} value={`item-${index + 1}`}>
Item {index + 1}
<SelectItemIndicator render={<Check fontSize="small" aria-hidden />} />
</SelectItem>
Expand Down
56 changes: 38 additions & 18 deletions packages/mui-base/src/Select/Item/SelectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<Element>,
) {
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<Element | null>(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]);
Expand Down Expand Up @@ -139,7 +155,7 @@ const SelectItem = React.forwardRef(function SelectItem(
);
});

interface InnerSelectItemProps extends SelectItem.Props {
interface InnerSelectItemProps extends Omit<SelectItem.Props, 'value'> {
highlighted: boolean;
selected: boolean;
getItemProps: UseInteractionsReturn['getItemProps'];
Expand All @@ -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<HTMLElement>;
/**
* 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
*/
Expand All @@ -200,27 +220,27 @@ 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,
} as any;
Expand Down
9 changes: 2 additions & 7 deletions packages/mui-base/src/Select/Item/useSelectItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export function useSelectItem(params: useSelectItem.Parameters): useSelectItem.R
const {
disabled = false,
highlighted,
selected,
id,
ref: externalRef,
setOpen,
Expand Down Expand Up @@ -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);
}

Expand All @@ -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(
Expand Down
32 changes: 27 additions & 5 deletions packages/mui-base/src/Select/Positioner/SelectPositioner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useComponentRenderer } from '../../utils/useComponentRenderer';
import { useForkRef } from '../../utils/useForkRef';
import { useSelectPositioner } from './useSelectPositioner';
import { HTMLElementType } from '../../utils/proptypes';
import { visuallyHidden } from '../../utils/visuallyHidden';

/**
* Renders the element that positions the Select popup.
Expand Down Expand Up @@ -65,7 +66,7 @@ const SelectPositioner = React.forwardRef(function SelectPositioner(
alignMethod,
innerFallback,
setInnerFallback,
selectedIndexOnMount,
selectedIndex,
} = useSelectRootContext();

const positioner = useSelectPositioner({
Expand All @@ -87,14 +88,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) {
Expand Down Expand Up @@ -150,21 +151,42 @@ const SelectPositioner = React.forwardRef(function SelectPositioner(
extraProps: otherProps,
});

const positionerElement = renderElement();

const selectItems = positionerElement.props.children.props.children as React.ReactElement[];
const mountedItemsElement = <div hidden>{selectItems}</div>;
const nativeSelectElement = (
<select style={visuallyHidden} tabIndex={-1} aria-hidden>
{selectItems.map((item) => (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<option key={item.props.value} value={item.props.value} />
))}
</select>
);

const shouldRender = keepMounted || mounted;
if (!shouldRender) {
return null;
return (
<SelectPositionerContext.Provider value={contextValue}>
<FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
{nativeSelectElement}
{mountedItemsElement}
</FloatingList>
</SelectPositionerContext.Provider>
);
}

return (
<SelectPositionerContext.Provider value={contextValue}>
<FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
{nativeSelectElement}
<FloatingPortal root={props.container}>
<FloatingFocusManager
context={positioner.floatingContext}
modal={false}
key={mounted.toString()}
>
{renderElement()}
{positionerElement}
</FloatingFocusManager>
</FloatingPortal>
</FloatingList>
Expand Down
12 changes: 12 additions & 0 deletions packages/mui-base/src/Select/Root/SelectRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ function SelectRoot(props: SelectRoot.Props) {
const {
animated = true,
children,
value,
defaultValue,
defaultOpen = false,
disabled = false,
loop = true,
Expand All @@ -24,6 +26,8 @@ function SelectRoot(props: SelectRoot.Props) {
defaultOpen,
open,
alignMethod,
value,
defaultValue,
});

const context: SelectRootContext = React.useMemo(
Expand All @@ -48,6 +52,14 @@ namespace SelectRoot {
*/
animated?: boolean;
children: React.ReactNode;
/**
* The value of the select.
*/
value?: string;
/**
* The default value of the select.
*/
defaultValue?: string;
/**
* If `true`, the Menu is initially open.
*
Expand Down
Loading

0 comments on commit 7bed7cc

Please sign in to comment.