-
Notifications
You must be signed in to change notification settings - Fork 639
ActionBar: Add support for groups #6979
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
base: main
Are you sure you want to change the base?
Changes from all commits
85ccd4d
2b1871d
f8e21be
dbcc75b
b2bff28
20dd785
29f7f97
8eed237
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 | ||
--- | ||
|
||
ActionBar: Adds `ActionBar.Group` sub component |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,3 +33,7 @@ | |
background: var(--borderColor-muted); | ||
} | ||
} | ||
|
||
.Group { | ||
display: flex; | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -26,8 +26,11 @@ type ChildProps = | |||||||||
icon: ActionBarIconButtonProps['icon'] | ||||||||||
onClick: MouseEventHandler | ||||||||||
width: number | ||||||||||
groupId?: string | ||||||||||
groupLabel?: string | ||||||||||
} | ||||||||||
| {type: 'divider'; width: number} | ||||||||||
| {type: 'group'; width: number; label: string} | ||||||||||
|
||||||||||
/** | ||||||||||
* Registry of descendants to render in the list or menu. To preserve insertion order across updates, children are | ||||||||||
|
@@ -38,9 +41,18 @@ type ChildRegistry = ReadonlyMap<string, ChildProps | null> | |||||||||
const ActionBarContext = React.createContext<{ | ||||||||||
size: Size | ||||||||||
registerChild: (id: string, props: ChildProps) => void | ||||||||||
unregisterChild: (id: string) => void | ||||||||||
unregisterChild: (id: string, groupId?: string) => void | ||||||||||
isVisibleChild: (id: string) => boolean | ||||||||||
}>({size: 'medium', registerChild: () => {}, unregisterChild: () => {}, isVisibleChild: () => true}) | ||||||||||
groupId?: string | ||||||||||
groupLabel?: string | ||||||||||
}>({ | ||||||||||
size: 'medium', | ||||||||||
registerChild: () => {}, | ||||||||||
unregisterChild: () => {}, | ||||||||||
isVisibleChild: () => true, | ||||||||||
groupId: undefined, | ||||||||||
groupLabel: undefined, | ||||||||||
}) | ||||||||||
|
||||||||||
/* | ||||||||||
small (28px), medium (32px), large (40px) | ||||||||||
|
@@ -107,7 +119,10 @@ const getMenuItems = ( | |||||||||
childRegistry: ChildRegistry, | ||||||||||
hasActiveMenu: boolean, | ||||||||||
): Set<string> | void => { | ||||||||||
const registryEntries = Array.from(childRegistry).filter((entry): entry is [string, ChildProps] => entry[1] !== null) | ||||||||||
const registryEntries = Array.from(childRegistry).filter( | ||||||||||
(entry): entry is [string, ChildProps] => | ||||||||||
entry[1] !== null && (entry[1].type !== 'action' || entry[1].groupId === undefined), | ||||||||||
) | ||||||||||
|
||||||||||
if (registryEntries.length === 0) return new Set() | ||||||||||
const numberOfItemsPossible = calculatePossibleItems(registryEntries, navWidth) | ||||||||||
|
@@ -248,11 +263,11 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop | |||||||||
|
||||||||||
if (menuItem.type === 'divider') { | ||||||||||
return <ActionList.Divider key={id} /> | ||||||||||
} else { | ||||||||||
} else if (menuItem.type === 'action' && !menuItem.groupLabel) { | ||||||||||
const {onClick, icon: Icon, label, disabled} = menuItem | ||||||||||
return ( | ||||||||||
<ActionList.Item | ||||||||||
key={label} | ||||||||||
key={id} | ||||||||||
// eslint-disable-next-line primer-react/prefer-action-list-item-onselect | ||||||||||
onClick={(event: React.MouseEvent<HTMLLIElement, MouseEvent>) => { | ||||||||||
closeOverlay() | ||||||||||
|
@@ -268,6 +283,49 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop | |||||||||
</ActionList.Item> | ||||||||||
) | ||||||||||
} | ||||||||||
|
||||||||||
// TODO: refine this so that we don't have to loop through the registry multiple times | ||||||||||
const groupedItems = Array.from(childRegistry).filter(([, childProps]) => { | ||||||||||
if (childProps?.type !== 'action') return false | ||||||||||
if (childProps.groupId !== id) return false | ||||||||||
return true | ||||||||||
}) | ||||||||||
|
||||||||||
if (menuItem.type === 'group') { | ||||||||||
return ( | ||||||||||
<ActionMenu key={id}> | ||||||||||
<ActionMenu.Anchor> | ||||||||||
<ActionList.Item>{menuItem.label}</ActionList.Item> | ||||||||||
</ActionMenu.Anchor> | ||||||||||
<ActionMenu.Overlay> | ||||||||||
<ActionList> | ||||||||||
{groupedItems.map(([key, childProps]) => { | ||||||||||
if (childProps && childProps.type === 'action') { | ||||||||||
const {onClick, icon: Icon, label, disabled} = childProps | ||||||||||
return ( | ||||||||||
<ActionList.Item | ||||||||||
key={key} | ||||||||||
onSelect={event => { | ||||||||||
closeOverlay() | ||||||||||
focusOnMoreMenuBtn() | ||||||||||
typeof onClick === 'function' && onClick(event as React.MouseEvent<HTMLElement>) | ||||||||||
}} | ||||||||||
disabled={disabled} | ||||||||||
> | ||||||||||
<ActionList.LeadingVisual> | ||||||||||
<Icon /> | ||||||||||
</ActionList.LeadingVisual> | ||||||||||
{label} | ||||||||||
</ActionList.Item> | ||||||||||
) | ||||||||||
} | ||||||||||
return null | ||||||||||
})} | ||||||||||
</ActionList> | ||||||||||
</ActionMenu.Overlay> | ||||||||||
</ActionMenu> | ||||||||||
) | ||||||||||
} | ||||||||||
})} | ||||||||||
</ActionList> | ||||||||||
</ActionMenu.Overlay> | ||||||||||
|
@@ -286,6 +344,7 @@ export const ActionBarIconButton = forwardRef( | |||||||||
const id = useId() | ||||||||||
|
||||||||||
const {size, registerChild, unregisterChild, isVisibleChild} = React.useContext(ActionBarContext) | ||||||||||
const {groupId} = React.useContext(ActionBarGroupContext) | ||||||||||
|
||||||||||
// Storing the width in a ref ensures we don't forget about it when not visible | ||||||||||
const widthRef = useRef<number>() | ||||||||||
|
@@ -302,9 +361,12 @@ export const ActionBarIconButton = forwardRef( | |||||||||
disabled: !!disabled, | ||||||||||
onClick: onClick as MouseEventHandler, | ||||||||||
width: widthRef.current, | ||||||||||
groupId: groupId ?? undefined, // todo: remove conditional | ||||||||||
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. Remove the unnecessary conditional and TODO comment. The
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
}) | ||||||||||
|
||||||||||
return () => unregisterChild(id) | ||||||||||
return () => { | ||||||||||
unregisterChild(id) | ||||||||||
} | ||||||||||
Comment on lines
+367
to
+369
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. [nitpick] The cleanup function can be simplified to
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
}, [registerChild, unregisterChild, props['aria-label'], props.icon, disabled, onClick]) | ||||||||||
|
||||||||||
const clickHandler = useCallback( | ||||||||||
|
@@ -315,7 +377,7 @@ export const ActionBarIconButton = forwardRef( | |||||||||
[disabled, onClick], | ||||||||||
) | ||||||||||
|
||||||||||
if (!isVisibleChild(id)) return null | ||||||||||
if (!isVisibleChild(id) || (groupId && !isVisibleChild(groupId))) return null | ||||||||||
|
||||||||||
return ( | ||||||||||
<IconButton | ||||||||||
|
@@ -325,11 +387,54 @@ export const ActionBarIconButton = forwardRef( | |||||||||
onClick={clickHandler} | ||||||||||
{...props} | ||||||||||
variant="invisible" | ||||||||||
data-testid={id} | ||||||||||
/> | ||||||||||
) | ||||||||||
}, | ||||||||||
) | ||||||||||
|
||||||||||
const ActionBarGroupContext = React.createContext<{ | ||||||||||
groupId: string | null | ||||||||||
label: string | undefined | ||||||||||
}>({groupId: null, label: undefined}) | ||||||||||
|
||||||||||
type ActionBarGroupProps = { | ||||||||||
label: string | ||||||||||
} | ||||||||||
|
||||||||||
export const ActionBarGroup = forwardRef( | ||||||||||
({label, children}: React.PropsWithChildren<ActionBarGroupProps>, forwardedRef) => { | ||||||||||
const backupRef = useRef<HTMLDivElement>(null) | ||||||||||
const ref = (forwardedRef ?? backupRef) as RefObject<HTMLDivElement> | ||||||||||
const id = useId() | ||||||||||
const {registerChild, unregisterChild} = React.useContext(ActionBarContext) | ||||||||||
|
||||||||||
// Like IconButton, we store the width in a ref ensures we don't forget about it when not visible | ||||||||||
// If a child has a groupId, it won't be visible if the group isn't visible, so we don't need to check isVisibleChild here | ||||||||||
const widthRef = useRef<number>() | ||||||||||
|
||||||||||
useIsomorphicLayoutEffect(() => { | ||||||||||
const width = ref.current?.getBoundingClientRect().width | ||||||||||
if (width) widthRef.current = width | ||||||||||
if (!widthRef.current) return | ||||||||||
|
||||||||||
registerChild(id, {type: 'group', width: widthRef.current, label}) | ||||||||||
|
||||||||||
return () => { | ||||||||||
unregisterChild(id) | ||||||||||
} | ||||||||||
}, [registerChild, unregisterChild]) | ||||||||||
|
||||||||||
return ( | ||||||||||
<ActionBarGroupContext.Provider value={{groupId: id, label}}> | ||||||||||
<div className={styles.Group} ref={ref}> | ||||||||||
{children} | ||||||||||
</div> | ||||||||||
</ActionBarGroupContext.Provider> | ||||||||||
) | ||||||||||
}, | ||||||||||
) | ||||||||||
|
||||||||||
export const VerticalDivider = () => { | ||||||||||
const ref = useRef<HTMLDivElement>(null) | ||||||||||
const id = useId() | ||||||||||
|
This comment was marked as spam.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.