-
Notifications
You must be signed in to change notification settings - Fork 641
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
Changes from 13 commits
85ccd4d
2b1871d
f8e21be
dbcc75b
b2bff28
20dd785
29f7f97
8eed237
7538ee4
87cb0c3
cbd06b7
4bbe643
8035d0a
f1bf903
b0a816e
77076a3
740ee43
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} | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * 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) | ||||||||||
|
|
@@ -223,6 +238,19 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop | |||||||||
| focusOutBehavior: 'wrap', | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| const groupedItems = React.useMemo(() => { | ||||||||||
| const groupedItemsMap = new Map<string, Array<[string, ChildProps]>>() | ||||||||||
|
|
||||||||||
| for (const [key, childProps] of childRegistry) { | ||||||||||
| if (childProps?.type === 'action' && childProps.groupId) { | ||||||||||
| const existingGroup = groupedItemsMap.get(childProps.groupId) || [] | ||||||||||
| existingGroup.push([key, childProps]) | ||||||||||
| groupedItemsMap.set(childProps.groupId, existingGroup) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| return groupedItemsMap | ||||||||||
| }, [childRegistry]) | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <ActionBarContext.Provider value={{size, registerChild, unregisterChild, isVisibleChild}}> | ||||||||||
| <div ref={navRef} className={clsx(className, styles.Nav)} data-flush={flush}> | ||||||||||
|
|
@@ -248,11 +276,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') { | ||||||||||
| 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 +296,40 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop | |||||||||
| </ActionList.Item> | ||||||||||
| ) | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Use the memoized map instead of filtering each time | ||||||||||
| const groupedMenuItems = groupedItems.get(id) || [] | ||||||||||
|
|
||||||||||
| // If we ever add additional types, this condition will be necessary | ||||||||||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||||||||||
| if (menuItem.type === 'group') { | ||||||||||
| return ( | ||||||||||
| <React.Fragment key={id}> | ||||||||||
| {groupedMenuItems.map(([key, childProps]) => { | ||||||||||
| if (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 | ||||||||||
| })} | ||||||||||
| </React.Fragment> | ||||||||||
| ) | ||||||||||
| } | ||||||||||
| })} | ||||||||||
| </ActionList> | ||||||||||
| </ActionMenu.Overlay> | ||||||||||
|
|
@@ -286,6 +348,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 +365,12 @@ export const ActionBarIconButton = forwardRef( | |||||||||
| disabled: !!disabled, | ||||||||||
| onClick: onClick as MouseEventHandler, | ||||||||||
| width: widthRef.current, | ||||||||||
| groupId: groupId ?? undefined, | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| return () => unregisterChild(id) | ||||||||||
| return () => { | ||||||||||
| unregisterChild(id) | ||||||||||
| } | ||||||||||
|
Comment on lines
+368
to
+370
|
||||||||||
| return () => { | |
| unregisterChild(id) | |
| } | |
| return () => unregisterChild(id) |
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.
Do we need this
groupLabel? I'm not sure if its used anywhereThere 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.
Good catch, we don't, I removed it!