-
Notifications
You must be signed in to change notification settings - Fork 538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixes for ActionList
semantics
#4272
Changes from 39 commits
7f21a16
fa708b1
e087f87
76e2606
eab6fd9
8b4f311
614749a
bfb8fe2
4b18eed
d13668d
10d9866
0939a90
cc6a63c
1cd9fb1
3d1daf3
5999a77
9f77b09
087ab1f
f4a5021
ba31e40
8e2921d
afa8d9f
4b1f2b4
10cd551
65c23c8
70659ed
1a9b3e0
1f783fa
6774f93
5ed547f
cf5ecf9
2f13c76
f32f416
84f041e
55632b1
8bc5c15
ce70779
3e8af88
4c8a4de
0d076dc
d2faa9a
54eab93
d1c1d60
9bbc8a2
1ceaa0c
d0a6c54
77027ad
0298349
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,5 @@ | ||||||
--- | ||||||
'@primer/react': minor | ||||||
--- | ||||||
|
||||||
Utilizes `<button>` inside of `ActionList.Item` instead of relying on `<li>` as an interactive item. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is great, thank you! 😁 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import theme from '../theme' | |
import {ActionList} from '.' | ||
import {behavesAsComponent, checkExports} from '../utils/testing' | ||
import {BaseStyles, ThemeProvider, SSRProvider, ActionMenu} from '..' | ||
import {FeatureFlags} from '../FeatureFlags' | ||
broccolinisoup marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function SimpleActionList(): JSX.Element { | ||
return ( | ||
|
@@ -378,4 +379,45 @@ describe('ActionList', () => { | |
const heading = getByText('Group Heading') | ||
expect(list).toHaveAttribute('aria-label', heading.textContent) | ||
}) | ||
|
||
it('should render ActionList.Item as button when feature flag is enabled', async () => { | ||
const {container} = HTMLRender( | ||
<FeatureFlags flags={{primer_react_action_list_item_as_button: true}}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because this is tests, I don't think it is super important to memoize or initialise the ff object outside of render but I wonder if we should do it anyway in case if anyone refers here for the practice? 🤔 What do you think? const ff = {primer_react_action_list_item_as_button: true}
<FeatureFlags flags={ff}>...</FeatureFlags> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! I've added your suggestion! |
||
<ActionList> | ||
<ActionList.Item disabled={true}>Item 1</ActionList.Item> | ||
<ActionList.Item>Item 2</ActionList.Item> | ||
</ActionList> | ||
</FeatureFlags>, | ||
) | ||
|
||
const button = container.querySelector('button') | ||
expect(button).toHaveTextContent('Item 1') | ||
|
||
// Ensure passed prop "disabled" is applied to the button | ||
expect(button).toHaveAttribute('aria-disabled', 'true') | ||
|
||
const listItems = container.querySelectorAll('li') | ||
expect(listItems.length).toBe(2) | ||
}) | ||
|
||
it('should render ActionList.Item as li when feature flag is disabled', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we want to test some of the listSemantics conditions as well or is it not necessary? For example;
Just a suggestion! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is a great suggestion! Just added a new test! |
||
const {container} = HTMLRender( | ||
<FeatureFlags flags={{primer_react_action_list_item_as_button: false}}> | ||
<ActionList> | ||
<ActionList.Item>Item 1</ActionList.Item> | ||
<ActionList.Item>Item 2</ActionList.Item> | ||
</ActionList> | ||
</FeatureFlags>, | ||
) | ||
|
||
const listitem = container.querySelector('li') | ||
const button = container.querySelector('button') | ||
|
||
expect(listitem).toHaveTextContent('Item 1') | ||
expect(listitem).toHaveAttribute('tabindex', '0') | ||
expect(button).toBeNull() | ||
|
||
const listItems = container.querySelectorAll('li') | ||
expect(listItems.length).toBe(2) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -21,6 +21,7 @@ import {getVariantStyles, ItemContext, TEXT_ROW_HEIGHT, ListContext} from './sha | |||
import type {VisualProps} from './Visuals' | ||||
import {LeadingVisual, TrailingVisual} from './Visuals' | ||||
import {ConditionalWrapper} from '../internal/components/ConditionalWrapper' | ||||
import {useFeatureFlag} from '../FeatureFlags' | ||||
|
||||
const LiBox = styled.li<SxProp>(sx) | ||||
|
||||
|
@@ -77,6 +78,8 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>( | |||
const {container, afterSelect, selectionAttribute, defaultTrailingVisual} = | ||||
React.useContext(ActionListContainerContext) | ||||
|
||||
const buttonSemantics = useFeatureFlag('primer_react_action_list_item_as_button') | ||||
|
||||
// Be sure to avoid rendering the container unless there is a default | ||||
const wrappedDefaultTrailingVisual = defaultTrailingVisual ? ( | ||||
<TrailingVisual>{defaultTrailingVisual}</TrailingVisual> | ||||
|
@@ -95,7 +98,7 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>( | |||
|
||||
const onSelect = React.useCallback( | ||||
( | ||||
event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>, | ||||
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if loosening the type here (which I think is a good way to eliminate the integration issue with dotcom) might have any "tangible" disadvantages? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's definitely not as strict, but it does ease the integration in dotcom. I think with the feature flag it makes it a bit trickier as the type is dependent on the FF being enabled. The issue I had when I was dealing with TS early on was that there are two potential things it could be; a list item or a button. In addition to that, the I did try adding both types There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I remember that! Let's see how this type works then and since it is under ff, we can always work on it! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hello from the future! This came up during release conductor stuff this week in dotcom, it seems like this change will cause typed handlers to fail since they are using the more specific type. Since we've switched to a more generic type, these more specific ones will fail 😭 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey Josh! This was a tricky situation since the current type I made the types more generic in dotcom in my integration test PR that should clean up the failures you're seeing. It does involve making the type a bit more generic, but we don't seem to lose out on too much. Let me know what you think though! |
||||
// eslint-disable-next-line @typescript-eslint/ban-types | ||||
afterSelect?: Function, | ||||
) => { | ||||
|
@@ -146,6 +149,12 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>( | |||
}, | ||||
} | ||||
|
||||
const listItemStyles = { | ||||
display: 'flex', | ||||
// show between 2 items | ||||
':not(:first-of-type)': {'--divider-color': theme?.colors.actionListItem.inlineDivider}, | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just checking, Is this and the following line conflicting? react/packages/react/src/ActionList/Item.tsx Line 216 in 087ab1f
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this was added to fix some styles when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember sorry 😢 It has been a while since I pushed that draft PR. We can comment it out and see how the stlyes are different to figure out if it still works fine in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went ahead and removed it, and it seems like it was needed for cases where a divider is used and the "button" semantics are applied. I think it's worth keeping for this 🤔 |
||||
} | ||||
|
||||
const styles = { | ||||
position: 'relative', | ||||
display: 'flex', | ||||
|
@@ -231,15 +240,15 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>( | |||
} | ||||
|
||||
const clickHandler = React.useCallback( | ||||
(event: React.MouseEvent<HTMLLIElement>) => { | ||||
(event: React.MouseEvent<HTMLElement>) => { | ||||
if (disabled || inactive) return | ||||
onSelect(event, afterSelect) | ||||
}, | ||||
[onSelect, disabled, inactive, afterSelect], | ||||
) | ||||
|
||||
const keyPressHandler = React.useCallback( | ||||
(event: React.KeyboardEvent<HTMLLIElement>) => { | ||||
(event: React.KeyboardEvent<HTMLElement>) => { | ||||
if (disabled || inactive) return | ||||
if ([' ', 'Enter'].includes(event.key)) { | ||||
if (event.key === ' ') { | ||||
|
@@ -259,8 +268,24 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>( | |||
const inlineDescriptionId = `${itemId}--inline-description` | ||||
const blockDescriptionId = `${itemId}--block-description` | ||||
const inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined | ||||
const validRole = listRole === 'listbox' || listRole === 'menu' || inactive | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we can rename the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is fair, I can add both, but let me know if we'd rather go with one or the other! |
||||
|
||||
const ButtonItemWrapper = buttonSemantics | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can initialise the ButtonItemWrapper here without worrying about the value of ff and on line 288 we can render the proper wrapper based on our conditions. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, that is fair! Thank you for the suggestion! 😁 |
||||
? (React.forwardRef(({as: Component = 'button', children, ...props}, forwardedRef) => { | ||||
return ( | ||||
<Box | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why we didn't use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you are right, because both render as buttons semantically at the end of the day. Unless we need to utilize Button props, I think we are good with the Box! |
||||
as={Component as React.ElementType} | ||||
sx={merge<BetterSystemStyleObject>(styles, sxProp)} | ||||
ref={forwardedRef} | ||||
{...props} | ||||
> | ||||
{children} | ||||
</Box> | ||||
) | ||||
}) as PolymorphicForwardRefComponent<React.ElementType, ActionListItemProps>) | ||||
: React.Fragment | ||||
|
||||
const ItemWrapper = _PrivateItemWrapper || React.Fragment | ||||
const ItemWrapper = _PrivateItemWrapper || (validRole || !buttonSemantics ? React.Fragment : ButtonItemWrapper) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we also need to add NavList context check into this condition, don't we? let DefaultItemWrapper = React.Fragment
if(buttonSemantics) {
DefaultItemWrapper = validRole || navListContext ? React.Fragment : ButtonItemWrapper
}
const ItemWrapper = _PrivateItemWrapper || DefautlItemWrapper I don't mean to re-write the condition, it wasn't straightforward to read when I added the NavList (at least for me 😅) Do it however you feel comfortable 💖 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right! This is easier to read too. Thank you! I adjusted |
||||
|
||||
// only apply aria-selected and aria-checked to selectable items | ||||
const selectableRoles = ['menuitemradio', 'menuitemcheckbox', 'option'] | ||||
|
@@ -281,20 +306,44 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>( | |||
id: itemId, | ||||
} | ||||
|
||||
const containerProps = _PrivateItemWrapper ? {role: itemRole ? 'none' : undefined} : menuItemProps | ||||
let containerProps | ||||
let wrapperProps | ||||
|
||||
const wrapperProps = _PrivateItemWrapper ? menuItemProps : {} | ||||
if (buttonSemantics) { | ||||
containerProps = _PrivateItemWrapper | ||||
? {role: itemRole ? 'none' : undefined, ...props} | ||||
: // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||||
(validRole && {...menuItemProps, ...props, ref: forwardedRef}) || {} | ||||
|
||||
wrapperProps = _PrivateItemWrapper | ||||
? menuItemProps | ||||
: !validRole && { | ||||
...menuItemProps, | ||||
...props, | ||||
styles: merge<BetterSystemStyleObject>(styles, sxProp), | ||||
ref: forwardedRef, | ||||
} | ||||
} else { | ||||
containerProps = _PrivateItemWrapper ? {role: itemRole ? 'none' : undefined} : {...menuItemProps, ...props} | ||||
wrapperProps = _PrivateItemWrapper ? menuItemProps : {} | ||||
} | ||||
|
||||
return ( | ||||
<ItemContext.Provider | ||||
value={{variant, disabled, inactive: Boolean(inactiveText), inlineDescriptionId, blockDescriptionId}} | ||||
> | ||||
<LiBox | ||||
ref={forwardedRef} | ||||
sx={merge<BetterSystemStyleObject>(styles, sxProp)} | ||||
ref={buttonSemantics || validRole ? forwardedRef : null} | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should have the ref There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It switches between the inner |
||||
sx={ | ||||
buttonSemantics | ||||
? merge<BetterSystemStyleObject>( | ||||
validRole || _PrivateItemWrapper ? styles : listItemStyles, | ||||
validRole || _PrivateItemWrapper ? sxProp : {}, | ||||
) | ||||
: merge<BetterSystemStyleObject>(styles, sxProp) | ||||
} | ||||
data-variant={variant === 'danger' ? variant : undefined} | ||||
{...containerProps} | ||||
{...props} | ||||
> | ||||
<ItemWrapper {...wrapperProps}> | ||||
<Selection selected={selected} /> | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,15 @@ import React, {isValidElement} from 'react' | |
import styled from 'styled-components' | ||
import type {ActionListDividerProps, ActionListLeadingVisualProps, ActionListTrailingVisualProps} from '../ActionList' | ||
import {ActionList} from '../ActionList' | ||
import {ActionListContainerContext} from '../ActionList/ActionListContainerContext' | ||
import Box from '../Box' | ||
import Octicon from '../Octicon' | ||
import type {SxProp} from '../sx' | ||
import sx, {merge} from '../sx' | ||
import {defaultSxProp} from '../utils/defaultSxProp' | ||
import {useId} from '../hooks/useId' | ||
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect' | ||
import {useFeatureFlag} from '../FeatureFlags' | ||
|
||
const getSubnavStyles = (depth: number) => { | ||
return { | ||
|
@@ -31,9 +33,21 @@ export type NavListProps = { | |
const NavBox = styled.nav<SxProp>(sx) | ||
|
||
const Root = React.forwardRef<HTMLElement, NavListProps>(({children, ...props}, ref) => { | ||
const listSemantics = useFeatureFlag('action_list_item_as_button') | ||
|
||
return ( | ||
<NavBox {...props} ref={ref}> | ||
<ActionList>{children}</ActionList> | ||
{listSemantics ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can use the context provider here regardless of the ff? |
||
<ActionListContainerContext.Provider | ||
value={{ | ||
container: 'NavList', | ||
}} | ||
> | ||
<ActionList>{children}</ActionList> | ||
</ActionListContainerContext.Provider> | ||
) : ( | ||
<ActionList>{children}</ActionList> | ||
)} | ||
</NavBox> | ||
) | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the best way to convey that this feature is feature flagged and not available outside of the flag? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious to learn that as well!