-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
feat: focustrap standalone service #2322
Conversation
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.
As discussed, two use cases need to be addressed:
- the case when focustrap is around something that is appended to body
- it might be better to call
this._focusInitial()
when the zone is stable
src/util/focus-trap.ts
Outdated
|
||
/** Detect if incoming focus event should be prevented or not */ | ||
private _enforceFocus(event) { | ||
const {shiftKey, target} = event; |
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.
shiftKey
is not used
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.
Removed !
src/util/focus-trap.spec.ts
Outdated
return element.querySelector(selector) as HTMLElement; | ||
}; | ||
|
||
function dispatchEvent(element, eventName: string, props: {[key: string]: any}) { |
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.
dispatchEvent
is not used
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.
Removed !
src/util/focus-trap.ts
Outdated
set autofocus(value: boolean) { | ||
if (value === true && this._previouslyFocused === null) { | ||
this._previouslyFocused = document.activeElement as HTMLElement; | ||
this._focusInitial(); |
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.
This would be called synchronously, I'm not sure it will work for dynamically instantiated components.
I guess it would try to focus before the change detection is run, so should be checked
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.
I am not sure it might be needed. Live tests seem to show that it is working in both datepicker and modal without that change.
Nonetheless, if we would ever need such a thing, I would rather prefer creating the focusTrap inside of an ngAfterViewInit
to make sure that View is created and hence available to focus.
@maxokorokov I pushed a new commit, and added a small change to make sure it would work properly in modal. The issue with modal is simple. Modal are appended to the body, so when you hit tab on the last focusable element of the modal, you basically have reached the end of the document. So we never go inside the Fix was simple, when focustrap is created I create a focusable anchor and append it to the body. I added a test for that also. |
d637e20
to
b704777
Compare
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.
Hey, @benouat, thanks!
LGTM, just a couple of questions about NgbFocusTrap
API after I did another pass. No big deal, just food for thought :)
Will merge tomorrow after you decide if it's worth fixing.
Feel free to open other PRs for Datepicker/Modal if you have time for it.
src/util/focus-trap.ts
Outdated
* Guess the next focusable element. | ||
* Computation is based on specific CSS selector and [tab] navigation direction | ||
*/ | ||
get focusableElement(): HTMLElement { |
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.
It is neither used publicly nor in tests. Why have a public getter for it instead of a private function?
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.
I used to have tests in one of the previous version. I will make it private
src/util/focus-trap.ts
Outdated
*/ | ||
create(element: HTMLElement, autofocus = false): NgbFocusTrap { | ||
const trap = new NgbFocusTrap(element, this._document, this._ngZone); | ||
trap.autofocus = autofocus; |
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.
Same question for autofocus setter. Do you see anybody calling it after constructing the service?
I mean doing service = factory.create(el)
and then doing service.autofocus = true
later, as opposed to service = factory.create(el, true)
right away ?
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.
You are right, it seems there is no added value to do that.
I will move the logic to constructor
right away.
Saucelabs failing as usual, will merge when green |
Awesome ! Thanks |
You got a bit fast on merging this one - btw my PRs are forever pending - since it has some drawbacks. First it doesn't handle the use case of coming from the outside of the viewport. Then it doesn't handle screen readers, where Also, since usually the Angular way is pushed, I thought that it would have been a good use case for a directive. Not to mention some other use cases not handled such as elements hidden because of a parent which is not displayed. So please don't overlook team work, I've worked on this task for quite a while now (doing researches to know what would be best for this product) so I have some knowledge about it, and also have an implementation already done before which handles those mentioned use cases. |
PR introduces an internal service to setup a focus-trap feature to be added to any element dynamically created by a widget.
Usage context: any datepicker popup, popover, modals...
Usage example:
providers: [NgbFocusTrapFactory]
private _focusTrapFactory: NgbFocusTrapFactory
NgbFocusTrap
instancethis._focusTrap.destory()
You can also decorate a node contained inside element with
NgbAutofocus
attribute, and specify a second boolean argument to thecreate()
method:this will automatically set the focus on the input when the focus gets initialised. On top it will remenber any previously focused element, and restore it when destroyed (feature needed to properly implement accessible modals)