Skip to content

Commit

Permalink
Update items traversal
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks committed Aug 23, 2024
1 parent eb20aea commit 2384edd
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 32 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
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<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
1 change: 1 addition & 0 deletions docs/pages/base-ui/api/select-item.json
Original file line number Diff line number Diff line change
@@ -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" } },
Expand Down
8 changes: 7 additions & 1 deletion docs/pages/base-ui/api/select-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -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;"],
Expand Down
13 changes: 7 additions & 6 deletions docs/translations/api-docs/select-item/select-item.json
Original file line number Diff line number Diff line change
@@ -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 <code>true</code>, the menu will close when the menu item is clicked."
"description": "If <code>true</code>, the select will close when the select item is clicked."
},
"disabled": { "description": "If <code>true</code>, the menu item will be disabled." },
"id": { "description": "The id of the menu item." },
"disabled": { "description": "If <code>true</code>, the select item will be disabled." },
"id": { "description": "The id of the select item." },
"label": {
"description": "A text representation of the menu item&#39;s content. Used for keyboard text navigation matching."
"description": "A text representation of the select item&#39;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": {}
}
14 changes: 10 additions & 4 deletions docs/translations/api-docs/select-root/select-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
"description": "Determines the type of alignment mode. <code>selected-item</code> aligns the popup so that the selected item appears over the trigger, while <code>trigger</code> aligns the popup using standard anchor positioning."
},
"animated": {
"description": "If <code>true</code>, the Menu supports CSS-based animations and transitions. It is kept in the DOM until the animation completes."
"description": "If <code>true</code>, the Select supports CSS-based animations and transitions. It is kept in the DOM until the animation completes."
},
"defaultOpen": { "description": "If <code>true</code>, the Menu is initially open." },
"disabled": { "description": "If <code>true</code>, the Menu is disabled." },
"defaultOpen": { "description": "If <code>true</code>, the Select is initially open." },
"defaultValue": { "description": "The default value of the select." },
"disabled": { "description": "If <code>true</code>, the Select is disabled." },
"id": { "description": "The id of the Select." },
"loop": {
"description": "If <code>true</code>, 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 <code>defaultOpen</code>."
}
},
"readOnly": { "description": "If <code>true</code>, the Select is read-only." },
"required": { "description": "If <code>true</code>, the Select is required." },
"value": { "description": "The value of the select." }
},
"classDescriptions": {}
}
4 changes: 4 additions & 0 deletions packages/mui-base/src/Select/Item/SelectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ SelectItem.propTypes /* remove-proptypes */ = {
* The click handler for the select item.
*/
onClick: PropTypes.func,
/**
* The value of the select item.
*/
value: PropTypes.string.isRequired,
} as any;

export { SelectItem };
28 changes: 24 additions & 4 deletions packages/mui-base/src/Select/Positioner/SelectPositioner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ 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) {
if (childProps?.value != null) {
selectItems.push(child);
} else if (childProps?.children) {
selectItems.push(...findSelectItems(child));
Expand Down Expand Up @@ -82,8 +85,23 @@ const SelectPositioner = React.forwardRef(function SelectPositioner(
innerFallback,
setInnerFallback,
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,
Expand Down Expand Up @@ -168,14 +186,16 @@ const SelectPositioner = React.forwardRef(function SelectPositioner(

const positionerElement = renderElement();
const selectItems = findSelectItems(positionerElement);
const mountedItemsElement = <div hidden>{selectItems}</div>;
const mountedItemsElement = keepMounted ? null : <div hidden>{selectItems}</div>;
const nativeSelectElement = (
<select
id={id}
name={name}
disabled={disabled}
required={required}
style={visuallyHidden}
tabIndex={-1}
aria-hidden
// @ts-ignore
inert
>
{selectItems.map((item) => (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
Expand Down
76 changes: 64 additions & 12 deletions packages/mui-base/src/Select/Root/SelectRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +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,
Expand All @@ -35,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 <SelectRootContext.Provider value={context}>{children}</SelectRootContext.Provider>;
Expand All @@ -45,13 +53,37 @@ 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
*/
animated?: boolean;
children: React.ReactNode;
/**
* 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.
*/
Expand All @@ -61,7 +93,7 @@ namespace SelectRoot {
*/
defaultValue?: string;
/**
* If `true`, the Menu is initially open.
* If `true`, the Select is initially open.
*
* @default false
*/
Expand All @@ -80,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
Expand All @@ -109,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
Expand All @@ -120,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.
*/
Expand All @@ -145,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 };
6 changes: 5 additions & 1 deletion packages/mui-base/src/Select/Root/SelectRootContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;
alignMethod: 'selected-item' | 'trigger';
id: string | undefined;
name: string | undefined;
disabled: boolean;
required: boolean;
readOnly: boolean;
}

export const SelectRootContext = React.createContext<SelectRootContext | null>(null);
Expand Down

0 comments on commit 2384edd

Please sign in to comment.