Update August 20:
@mark_reeder and I had a follow up chat on the Base Web Slack. Mark explained to me that you can pass a component to the label
prop (as opposed to passing a string). So now, instead of overriding Option with my own custom component, I just pass a custom label component with an icon to label
. Much easier!Thanks again, @mark_reeder.
Update August 16: I accepted the answer of @mark_reeder. This fixed the issue of getting the focus states to work. However, clicking "Enter" to select the focused element still did not work, I ended up adding an event listener to the body and doing some kludegy logic. I'm hoping someone out there has a better way.
I'm new to Base Web. I built a custom "more options" menu (aka "kebab" menu). I wanted the options to have icons so I did an override on the Option in the Menu component. It looks good, but the keyboard bindings no longer work. If I comment out the override, the keyboard bindings work (but then I lose the icons).
Here is my code:
//MoreOptionsMenu.js
import React from 'react'
import { StatefulPopover, PLACEMENT } from 'baseui/popover'
import Overflow from 'baseui/icon/overflow'
import { StatefulMenu } from 'baseui/menu'
import IconOption from 'Components/Shared/IconOption/IconOption'
import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper'
const MoreOptionsMenu = ({ items, placement = PLACEMENT.bottom, ariaLabel, id }) => {
return (
<StatefulPopover
content={({ close }) => (
<StatefulMenu
items={items}
overrides={{
Option: {
component: IconOption,
props: {
close,
ariaLabel,
id
}
},
List: {
style: ({ $theme }) => ({
borderTopLeftRadius: '6px',
borderTopRightRadius: '6px',
borderBottomLeftRadius: '6px',
borderBottomRightRadius: '6px',
border: `1px solid ${$theme.colors.lightGray}`,
})
}
}}
/>
)}
accessibilityType={'tooltip'}
placement={placement}
>
<InvisibleButtonWrapper>
<Overflow size={24} aria-label={ariaLabel} style={{ marginLeft: 'auto', cursor: 'pointer' }}/>
</InvisibleButtonWrapper>
</StatefulPopover>
)
}
export default MoreOptionsMenu
//IconOptions
import React, { forwardRef } from 'react'
import { useStyletron } from 'baseui'
import { stringToKebabCase } from 'Shared/Utilities'
import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper'
const IconOption = forwardRef(( { close, item, ariaLabel, id }, ref) => {
const [css, theme] = useStyletron()
return (
<li
role='option'
aria-disabled='false'
aria-selected='false'
id={stringToKebabCase(`${id}-${item.label}`)}
className={css({
display: 'flex',
alignItems: 'center',
padding: '10px',
cursor: 'pointer',
':hover': {
outline: `${theme.colors.accent} solid 3px`
},
':focus': {
outline: `${theme.colors.accent} solid 3px`
},
':active': {
outline: `${theme.colors.accent} solid 3px`
},
':nth-child(even)': {
backgroundColor: theme.colors.lighterGray
}
})}
aria-labelledby={ariaLabel}
ref={ref}
onClick={() => {
typeof item.callback === 'function' && item.callback()
close()
}}>
<InvisibleButtonWrapper>
{item.icon}
<span style={{ marginLeft: '10px' }}>{item.label}</span>
</InvisibleButtonWrapper>
</li>
)
})
export default IconOption
// InvisibleButtonWrapper.js
import { withStyle } from 'baseui'
import { StyledBaseButton } from 'baseui/button'
const InvisibleButtonWrapper = withStyle(StyledBaseButton, ({$theme}) => ({
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
color: `${$theme.colors.primaryA}`,
backgroundColor: 'inherit',
':hover': {
color: `${$theme.colors.primaryA}`,
backgroundColor: 'inherit',
}
}))
export default InvisibleButtonWrapper
Here is a sandbox so you can see / play with the code
A few more notes in anticipation of questions that people may have. The reason I use forwardRef is that I was getting an error about function components not being able to receive refs. The keyboard bindings do not work regardless of whether I use forwardRef. The InvisibleButtonWrapper is intended to make the component more accessible without breaking the styles. Removing it does not seem to affect the keyboard bindings.
Also, is there an idiomatic Base Web way to do this that I am missing? The docs say "Each menu item has an option to include an icon by default, but this can be removed." However, there is not example of how to make that work. Before I created my own custom component, I tried simply adding an icon property to the items but it did not render.
Latest file update with fixes from mark_reeder and aforementioned event listener on document:
import React, { forwardRef, useEffect } from 'react'
import { useStyletron } from 'baseui'
import { stringToKebabCase } from 'Shared/Utilities'
import InvisibleButtonWrapper from 'Components/Shared/InvisibleButtonWrapper/InvisibleButtonWrapper'
const IconOption = forwardRef((props, ref) => {
const [css, theme] = useStyletron()
const { close, item, ariaLabel, id, $isHighlighted, $isFocused } = props
const handleKeyDown = ({ code }) => {
if (code === 'Enter') {
document.querySelector("[data-iconoption='true'][aria-selected='true']").click()
}
}
useEffect(() => {
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [])
return (
<li
data-iconoption={'true'}
tabIndex={0}
role='option'
aria-selected={props['aria-selected'] ? 'true' : 'false'}
id={stringToKebabCase(`${id}-${item.label}`)}
className={css({
backgroundColor: $isHighlighted ? theme.colors.lighterGray : theme.colors.white,
display: 'flex',
alignItems: 'center',
padding: '10px',
cursor: 'pointer',
':hover': {
backgroundColor: theme.colors.lighterGray
},
':focus': {
backgroundColor: theme.colors.lighterGray
},
})}
aria-labelledby={ariaLabel}
ref={ref}
onClick={() => {
typeof item.callback === 'function' && item.callback()
close()
}}>
<InvisibleButtonWrapper>
{item.icon}
<span style={{ marginLeft: '10px' }}>{item.label}</span>
</InvisibleButtonWrapper>
</li>
)
})
export default IconOption
The base menu components rely on props to display the highlighted menu item: https://github.com/uber/baseweb/blob/master/src/menu/styled-components.tsx#L50
Two changes to your IconOption
will get your outlines showing up:
First, add $isHighlighted
to your props list for IconOption
-
const IconOption = forwardRef(({ $isHighlighted, close, item, ariaLabel, id }, ref) => {
Second, modify your outline
in the style of your li
in IconOption
based on the value of $isHighlighted
:
outline: $isHighlighted ? `${theme.colors.accent} solid 3px` : 'none',
As far as more Idiomatic Base Web ways of doing this, there are some examples that include images that might work for you like https://baseweb.design/components/menu/#menu-with-profile-menu