Repository navigation
Offer the workspace members bulk actions in the floating bar #102380
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
Open
Krishna2323
wants to merge
37
commits into
Expensify:main
Choose a base branch
from
Krishna2323:krishna2323/issue/101898-members-bulk-action-bar
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,142
−465
Open
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
ad64a1f
Offer the workspace members bulk actions in the floating bar
Krishna2323 c963cc9
Drop focus before Esc clears a selection, so no focus ring appears
Krishna2323 11fd6a4
fix knip check.
Krishna2323 b3b795a
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 db1fb97
Make the bulk action bar keyboard and screen reader accessible
Krishna2323 b6d297b
Let Tab move through the bulk action bar's actions
Krishna2323 eb20697
remove translations.
Krishna2323 98742eb
Drop the unused BulkActionBarFocus export
Krishna2323 59fa50d
Keep Enter on a bar button from also opening the focused row
Krishna2323 0d76bfb
Hand focus back whenever an action takes the bulk action bar away
Krishna2323 74a1517
Merge branch 'main' into krishna2323/issue/101898-members-bulk-action…
Krishna2323 df938d8
Consolidate member bulk role actions into a Change role selector
Krishna2323 695f332
Keep Save pressable on the bulk role selector and ask for a role instead
Krishna2323 da8dd4b
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 7ad0d18
Use the user pencil icon for the bulk Change role action
Krishna2323 b847c52
compress svg
Krishna2323 8bd44a8
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 869c111
update translations.
Krishna2323 f2309e2
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 16e9368
Register the members role screen with navigation
Krishna2323 6b132c7
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 b06f43c
Scope the stashed role-change selection to its workspace
Krishna2323 b7a71f5
Hand focus back when the bulk action bar goes away from another screen
Krishna2323 c67ba73
Make the bulk action bar and the role error answer the keyboard
Krishna2323 ed99ae6
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 74205d9
Keep the bulk action bar's hand-back target on the table
Krishna2323 669a14c
Restore focus to the table after a bulk action navigates
Krishna2323 7c1925c
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 e2f1301
Point the removed-approver test at the bulk action bar
Krishna2323 af23a5e
Merge branch 'Expensify:main' into krishna2323/issue/101898-members-b…
Krishna2323 149c56a
Find the bar's own popover through its launcher rather than through m…
Krishna2323 579091c
Merge branch 'Expensify:main' into krishna2323/issue/101898-members-b…
Krishna2323 1237383
Merge branch 'main' into krishna2323/issue/101898-members-bulk-action…
Krishna2323 21f3e43
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 5e43fb3
Merge branch 'Expensify:main' into krishna2323/issue/101898-members-b…
Krishna2323 d965210
Merge remote-tracking branch 'upstream/main' into krishna2323/issue/1…
Krishna2323 eb14294
Remember the control that made the selection
Krishna2323 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
src/components/BulkActionBar/useBulkActionBarFocus/index.native.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import type UseBulkActionBarFocus from './types'; | ||
|
|
||
| const noop = () => {}; | ||
|
|
||
| /** Native has no tab order and no focus ring, so the bar's controls are reached through the screen reader instead. */ | ||
| const useBulkActionBarFocus: UseBulkActionBarFocus = () => ({suppressStrayFocusRing: noop, isFocusInsideBar: false}); | ||
|
|
||
| export default useBulkActionBarFocus; |
156 changes: 156 additions & 0 deletions
156
src/components/BulkActionBar/useBulkActionBarFocus/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import isHTMLElement from '@libs/isHTMLElement'; | ||
| import {pickLauncher} from '@libs/LauncherStack'; | ||
| import {nominateTriggerFallback} from '@libs/NavigationFocusReturn'; | ||
| import markProgrammaticFocus from '@libs/programmaticFocus'; | ||
| import restoreFocusWithModality from '@libs/restoreFocusWithModality'; | ||
|
|
||
| import {useEffect, useLayoutEffect, useRef, useState} from 'react'; | ||
|
|
||
| import type UseBulkActionBarFocus from './types'; | ||
|
|
||
| /** | ||
| * Whether the browser is already drawing a focus ring on this element. Browsers that cannot be asked are assumed to be | ||
| * drawing one, so a ring is never taken away from someone who navigated there with the keyboard. | ||
| */ | ||
| function isShowingFocusRing(element: HTMLElement): boolean { | ||
| try { | ||
| return element.matches(':focus-visible'); | ||
| } catch { | ||
| return true; | ||
|
Krishna2323 marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| /** Tracks where focus is relative to the bar, and hands it back when the bar goes away holding it. */ | ||
| const useBulkActionBarFocus: UseBulkActionBarFocus = (barRef, isCoveredByScreen) => { | ||
| // Where focus was before the bar took it. The bar goes away with the selection, so focus has to be handed back. | ||
| const lastFocusedOutsideRef = useRef<HTMLElement | null>(null); | ||
|
Krishna2323 marked this conversation as resolved.
|
||
|
|
||
| const [isFocusInsideBar, setIsFocusInsideBar] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const remember = (target: EventTarget | null) => { | ||
| if (!isHTMLElement(target) || target === document.body) { | ||
| return; | ||
| } | ||
|
|
||
| const bar = barRef.current; | ||
| if (isHTMLElement(bar) && bar.contains(target)) { | ||
| return; | ||
| } | ||
|
|
||
| // The role screen and the bar's own "More" menu both take focus to controls that leave with them, and the | ||
| // menu renders in a popover outside the bar rather than within it. Remembering either would leave the bar | ||
| // holding a target that is already gone when it needs one. A popover is identified by the launcher it | ||
| // registered as it opened, which it does before it moves focus into itself. | ||
| if (isCoveredByScreen || pickLauncher()) { | ||
| return; | ||
| } | ||
|
|
||
| lastFocusedOutsideRef.current = target; | ||
| // An action that opens a screen is navigated back from, and the restore that follows aims at the bar | ||
| // button it was opened from, which the cleared selection has taken away by then. This is the survivor to | ||
| // aim at instead, and it is offered here rather than on unmount because the capture happens first. | ||
| nominateTriggerFallback(target); | ||
| }; | ||
|
|
||
| // The bar appears because of a selection, so the control that made it already holds focus and fires no | ||
| // focusin for the listener below to see. | ||
| if (!lastFocusedOutsideRef.current) { | ||
| remember(document.activeElement); | ||
| } | ||
|
|
||
| const handleFocusIn = (event: FocusEvent) => { | ||
| const target = event.target; | ||
| if (!isHTMLElement(target)) { | ||
| return; | ||
| } | ||
|
|
||
| const bar = barRef.current; | ||
| if (isHTMLElement(bar) && bar.contains(target)) { | ||
| setIsFocusInsideBar(true); | ||
| return; | ||
| } | ||
|
|
||
| setIsFocusInsideBar(false); | ||
| remember(target); | ||
| }; | ||
|
|
||
| // Focus can also leave for nothing at all, such as an element being removed, which fires no matching focusin. | ||
| const handleFocusOut = (event: FocusEvent) => { | ||
| if (event.relatedTarget) { | ||
| return; | ||
| } | ||
|
|
||
| setIsFocusInsideBar(false); | ||
| }; | ||
|
|
||
| document.addEventListener('focusin', handleFocusIn); | ||
| document.addEventListener('focusout', handleFocusOut); | ||
|
|
||
| return () => { | ||
| document.removeEventListener('focusin', handleFocusIn); | ||
| document.removeEventListener('focusout', handleFocusOut); | ||
| }; | ||
| }, [barRef, isCoveredByScreen]); | ||
|
|
||
| // Most of the bar's actions clear the selection themselves, so the bar is torn out from under the button that was | ||
| // just pressed and focus would fall to the document. Handing it back here covers every one of those actions, | ||
| // rather than only the two the bar runs itself. A layout cleanup rather than a passive one, because this has to | ||
| // read the focus before the bar leaves the DOM. | ||
| useLayoutEffect( | ||
| () => () => { | ||
| nominateTriggerFallback(null); | ||
|
|
||
| const bar = barRef.current; | ||
| const active = document.activeElement; | ||
|
|
||
| // An action run from a screen opened over the table clears the selection from there, so the bar is gone | ||
| // before that screen hands its own focus back and there is nothing left for it to hand back to. Focus has | ||
| // already fallen to the document by then, which is the same loss as the bar being torn out from under it. | ||
| const hasFocusFallenToDocument = !active || active === document.body; | ||
|
|
||
| // An action run from the bar's own "More" menu is pressed with focus inside a popover that renders outside | ||
| // the bar, so the bar does not contain the focus it nonetheless has to hand back. The popover that leaves | ||
| // with the bar is the one whose launcher the bar does contain. | ||
| const launcher = pickLauncher(); | ||
| const isFocusInsideBarPopover = isHTMLElement(bar) && !!launcher && bar.contains(launcher); | ||
| if (!hasFocusFallenToDocument && !isFocusInsideBarPopover && !(isHTMLElement(bar) && bar.contains(active))) { | ||
| return; | ||
| } | ||
|
|
||
| const previous = lastFocusedOutsideRef.current; | ||
| if (!previous?.isConnected) { | ||
| return; | ||
| } | ||
|
|
||
| // The screen that cleared the selection may still be unwinding its focus trap, which would otherwise pull | ||
| // focus straight back into the container it is closing. | ||
| restoreFocusWithModality(previous); | ||
| }, | ||
| [barRef], | ||
| ); | ||
|
|
||
| const suppressStrayFocusRing = () => { | ||
| const active = document.activeElement; | ||
| if (!isHTMLElement(active)) { | ||
| return; | ||
| } | ||
|
|
||
| const bar = barRef.current; | ||
| if (isHTMLElement(bar) && bar.contains(active)) { | ||
| return; | ||
| } | ||
|
|
||
| // Esc makes whatever holds focus match `:focus-visible`, so a checkbox clicked with the mouse would light up | ||
| // with a focus ring just as the selection it belongs to disappears. | ||
| if (isShowingFocusRing(active)) { | ||
| return; | ||
| } | ||
|
|
||
| markProgrammaticFocus(active); | ||
| }; | ||
|
|
||
| return {suppressStrayFocusRing, isFocusInsideBar}; | ||
| }; | ||
|
|
||
| export default useBulkActionBarFocus; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.