Skip to content

feat: S2 unavailable menu item#9657

Open
LFDanLu wants to merge 21 commits intomainfrom
s2_unavailable_menu_item
Open

feat: S2 unavailable menu item#9657
LFDanLu wants to merge 21 commits intomainfrom
s2_unavailable_menu_item

Conversation

@LFDanLu
Copy link
Member

@LFDanLu LFDanLu commented Feb 13, 2026

Closes

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

Test both the S2 and RAC implementations in both storybook and docs.

🧢 Your Project:

RSP

@rspbot
Copy link

rspbot commented Feb 13, 2026

@rspbot
Copy link

rspbot commented Feb 17, 2026


### Unavailable menu items

Wrap a `<MenuItem>` and a `<Popover>` with a `<UnavailableMenuItemTrigger>` and apply `isUnavailable` to disable the item's default action and show an informational popover instead.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to have put the Popover directly into the example rather than create a UnavailableMenuItemTrigger wrapper in the starters since I felt it might be confusing to have Popover mentioned here but not show up in the example. Open to opinions though, not too bothered if we wanna go the same route as SubmenuTrigger and wrap the Popover in the starters

Comment on lines 655 to 658
// TODO: consider if we should instead pull the RAC defined trigger here into S2. Feels useful for RAC users though, but
// maybe shouldn't be specific to unavailable. It is very similar to SubmenuTrigger but it renders a dialog type behavior (see useSubmenuTrigger)
// and has the concept of isUnavailable which will make the item render normally when false
function UnavailableMenuItemTrigger(props: UnavailableMenuItemTriggerProps): JSX.Element {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnavailableMenuItemTrigger seems a bit specific in RAC honestly, open to opinions if there should be a more generic naming/component

@LFDanLu LFDanLu changed the title feat: (WIP) S2 unavailable menu item feat: S2 unavailable menu item Feb 17, 2026
@rspbot
Copy link

rspbot commented Feb 17, 2026

@LFDanLu LFDanLu marked this pull request as ready for review February 18, 2026 18:49
Comment on lines 36 to 39
// TODO: this is quite specific, but description and keyboardShortcut above are also very specific
/** Props for the descriptive element in the end slot inside the menu item (e.g. info icon, chevron). */
endSlotProps: DOMAttributes,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite iffy about this, as stated above it is very specific. I think ideally we'd give the user a way to make arbitrary child elements in a MenuItem part of that MenuItem's aria-describedby. I could've opted to just allow the user to provide a generic aria-describedBy to the menu item and then apply ids to various other elements but we already have description and keyboard shortcut handled so figured we may be able to make end slot a thing too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that due to how collection items work in RAC (aka UnavailableMenuItemTrigger in RAC is a collection branch that only renders the popover child as is), I believe that RAC level change is unavoidable since there isn't a good way to generate a id at the S2 UnavailableMenuItemTrigger level and propagate it down via context

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about allowing 'aria-describedby' as a prop and merging that with the ones we generate? Then this could be generated by S2 MenuItem and passed in. The only question would be in what order do we do the merge?

Copy link
Member Author

@LFDanLu LFDanLu Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can go this route instead, just felt potentially confusing to the end user that they'd be expected to pass aria-describedby for some things but that we'd handle doing this for such specific things like keyboard shortcut icon and description.

I think we'd should merge the user's provided value first. Is the concern here the announcement order or more so what would happen if the user provided their own ids to the keyboard icon/description and then used those in their aria-described?

@rspbot
Copy link

rspbot commented Feb 19, 2026

@rspbot
Copy link

rspbot commented Feb 20, 2026

reidbarber
reidbarber previously approved these changes Feb 20, 2026
Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@rspbot
Copy link

rspbot commented Feb 20, 2026

@rspbot
Copy link

rspbot commented Feb 23, 2026

@rspbot
Copy link

rspbot commented Feb 23, 2026

## API Changes

react-aria-components

/react-aria-components:MenuItemRenderProps

 MenuItemRenderProps {
   allowsDragging?: boolean
   hasSubmenu: boolean
   isDisabled: boolean
   isDragging?: boolean
   isDropTarget?: boolean
   isFocusVisible: boolean
   isFocused: boolean
   isHovered: boolean
   isOpen: boolean
   isPressed: boolean
   isSelected: boolean
+  isUnavailable?: boolean
   selectionBehavior: SelectionBehavior
   selectionMode: SelectionMode
 }

/react-aria-components:UnavailableMenuItemTrigger

+UnavailableMenuItemTrigger {
+  children: Array<ReactElement>
+  isUnavailable?: boolean = false
+}

/react-aria-components:EndSlotContext

+EndSlotContext {
+  UNTYPED
+}

/react-aria-components:UnavailableMenuItemTriggerProps

+UnavailableMenuItemTriggerProps {
+  children: Array<ReactElement>
+  isUnavailable?: boolean = false
+}

@react-aria/menu

/@react-aria/menu:MenuItemAria

 MenuItemAria {
   descriptionProps: DOMAttributes
+  endSlotProps: DOMAttributes
   isDisabled: boolean
   isFocusVisible: boolean
   isFocused: boolean
   isPressed: boolean
   keyboardShortcutProps: DOMAttributes
   labelProps: DOMAttributes
   menuItemProps: DOMAttributes
 }

@react-spectrum/s2

/@react-spectrum/s2:UnavailableMenuItemTrigger

+UnavailableMenuItemTrigger {
+  children: Array<ReactElement>
+  isUnavailable?: boolean = false
+}

/@react-spectrum/s2:UnavailableMenuItemTriggerProps

+UnavailableMenuItemTriggerProps {
+  children: Array<ReactElement>
+  isUnavailable?: boolean = false
+}

reidbarber
reidbarber previously approved these changes Feb 23, 2026
*
* @version alpha
*/
export const UnavailableMenuItemTrigger = /*#__PURE__*/ createBranchComponent(UnavailableMenuItemTriggerNode, (props: UnavailableMenuItemTriggerProps, ref: ForwardedRef<HTMLDivElement>, item) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the differences between this and SubmenuTrigger? All of our popovers are dialogs now (even submenus) so it seems like we could use SubmenuTrigger for this already? Unavailable seems like a very spectrum concept to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The differentiating factor is the behavior differences that come into play when providing type: 'dialog' to the useSubmenuTrigger hook:

...(type === 'menu' && {
onClose: state.closeAll,
autoFocus: state.focusStrategy ?? undefined,
onKeyDown: submenuKeyDown
})

With submenus, hitting escape will close ALL levels of the menu tree, ArrowLeft will close the submenu, and we may autofocus the first item in the submenu.

I definitely agree with Unavailable being a spectrum concept though, as mentioned here https://github.com/adobe/react-spectrum/pull/9657/changes#r2849513111, happy to make this SubDialogTrigger instead perhaps

const MenuItemContext = createContext<ContextValue<MenuItemContextProps, HTMLDivElement>>(null);

/** Context for the descriptive element in the end slot of the menu item (e.g. info icon). Propagates the appropriate labelling props to associate the element with the parent menu item. */
export const EndSlotContext = createContext<ContextValue<{id?: string}, HTMLElement>>(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where this is consumed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consumed by S2 MenuItem:

let endSlotProps = useSlottedContext(EndSlotContext);
let {direction, size} = props;
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
return (
<div slot="descriptor" className={mergeStyles(descriptor, style({marginBottom: fontRelative(-1)}))} id={endSlotProps?.id}>


/**
* Transforms ContextualHelpTrigger:
* - Rename ContextualHelpTrigger to UnavailableMenuItemTrigger.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you discussed renaming this somewhere? Is it not specific to ContextualHelp anymore? I think the original idea was that it was also not specific to unavailable and people might use it to provide other kinds of help info but not sure we ever supported that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, didn't really discuss this anywhere, but a personal feeling that ContextualHelp (arbitrarily attached to any element) and ContextualHelpTrigger (menu) were too similarly named thus was confusing.

As for making it not specific to unavailable, I wasn't aware that was the direction we wanted to go as a team, but I had the same thought here. Happy to change this to be SubDialogTrigger or something along those line if so

Copy link
Member Author

@LFDanLu LFDanLu Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some additional thoughts: if it wasn't specific to unavailable (and were more just informational), would that imply that the wrapped menu item should still be actionable? If so, I guess we'll need to add another prop so the user can decide whether or not the menu level onAction makes it to the item at the RAC level. At the S2 level, it would also need to differentiate between the isUnavailable=true case, the isUnavailable=false case, and the "is rendering a informational subdialog case"

Comment on lines 36 to 39
// TODO: this is quite specific, but description and keyboardShortcut above are also very specific
/** Props for the descriptive element in the end slot inside the menu item (e.g. info icon, chevron). */
endSlotProps: DOMAttributes,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about allowing 'aria-describedby' as a prop and merging that with the ones we generate? Then this could be generated by S2 MenuItem and passed in. The only question would be in what order do we do the merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants