-
Notifications
You must be signed in to change notification settings - Fork 8
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
Support dynamic tabbableChildren for TableCellView #2340
Changes from 1 commit
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 |
---|---|---|
@@ -1,5 +1,13 @@ | ||
/* eslint-disable no-await-in-loop */ | ||
import { customElement, html, observable, ref } from '@microsoft/fast-element'; | ||
import { | ||
children, | ||
customElement, | ||
elements, | ||
html, | ||
observable, | ||
ref, | ||
when | ||
} from '@microsoft/fast-element'; | ||
import { FoundationElement } from '@microsoft/fast-foundation'; | ||
import { | ||
keyArrowDown, | ||
|
@@ -49,6 +57,7 @@ import type { ColumnInternalsOptions } from '../../table-column/base/models/colu | |
import { ColumnValidator } from '../../table-column/base/models/column-validator'; | ||
import { mixinSortableColumnAPI } from '../../table-column/mixins/sortable-column'; | ||
import { MenuButtonPageObject } from '../../menu-button/testing/menu-button.pageobject'; | ||
import { dynamicRef } from '../../utilities/directive/dynamic-ref'; | ||
|
||
interface SimpleTableRecord extends TableRecord { | ||
id: string; | ||
|
@@ -1238,15 +1247,18 @@ describe('Table keyboard navigation', () => { | |
// prettier-ignore | ||
@customElement({ | ||
name: interactiveCellViewName, | ||
template: html<TestInteractiveCellView>`<span tabindex="-1" ${ref('spanElement')}>Test</span>` | ||
template: html<TestInteractiveCellView>`${when(x => x.isTabbable, html<TestInteractiveCellView>`<span tabindex="-1" ${dynamicRef('spanElement')}>Test</span>`)}` | ||
}) | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
class TestInteractiveCellView extends TableCellView { | ||
@observable | ||
public spanElement!: HTMLSpanElement; | ||
public isTabbable = true; | ||
|
||
@observable | ||
public spanElement?: HTMLSpanElement; | ||
|
||
public override get tabbableChildren(): HTMLElement[] { | ||
return [this.spanElement]; | ||
private spanElementChanged(): void { | ||
this.tabbableChildren = this.spanElement ? [this.spanElement] : []; | ||
} | ||
} | ||
// prettier-ignore | ||
|
@@ -1286,7 +1298,7 @@ describe('Table keyboard navigation', () => { | |
rowIndex, | ||
columnIndex | ||
) as TestInteractiveCellView | ||
).spanElement; | ||
).spanElement!; | ||
} | ||
|
||
beforeEach(async () => { | ||
|
@@ -1425,6 +1437,25 @@ describe('Table keyboard navigation', () => { | |
expect(blurSpy).toHaveBeenCalledTimes(1); | ||
expect(currentFocusedElement()).not.toBe(cellContent); | ||
}); | ||
|
||
it('and then the cell updates to no longer have tabbableChildren, the cell is focused instead', 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. I presume if we went with this approach (which I think I'm fine with), that there are some other tests we still need to add, yes? Namely, that after the tabbableChildren change (but not completely removed, as this test covers that) that tabbing (and shift-tabbing) behave as expected. 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. Agreed, those would be good tests to add too. |
||
const cellView = pageObject.getRenderedCellView( | ||
0, | ||
1 | ||
) as TestInteractiveCellView; | ||
cellView.isTabbable = false; | ||
await waitForUpdatesAsync(); | ||
|
||
// Note: At this point, the table lost focus already, because the focused element in the cell has been removed from the DOM, and | ||
// KeyboardNavigationManager will only set focus to new elements when the table is already focused (so we don't steal focus from | ||
// elsewhere on the page if the table isn't being interacted with). | ||
element.focus(); | ||
await waitForUpdatesAsync(); | ||
|
||
expect(currentFocusedElement()).toBe( | ||
pageObject.getCell(0, 1) | ||
); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { | ||
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. Does FAST have unit tests for their RefBehavior that we could leverage for patterns to write unit tests for this class? 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. Not that I've found, but writing our own should be pretty quick anyway. |
||
AttachedBehaviorHTMLDirective, | ||
type Behavior, | ||
type CaptureType | ||
} from '@microsoft/fast-element'; | ||
|
||
/** | ||
* A directive that updates a property with a reference to the element. | ||
* Similar to RefBehavior, but sets the property back to undefined when unbound | ||
* (when the source element is removed from the DOM). | ||
* @public | ||
*/ | ||
export class DynamicRefBehavior implements Behavior { | ||
private readonly target: unknown; | ||
private propertyName: string; | ||
|
||
/** | ||
* Creates an instance of DynamicRefBehavior. | ||
* @param target - The element to reference. | ||
* @param propertyName - The name of the property to assign the reference to. | ||
*/ | ||
public constructor(target: unknown, propertyName: string) { | ||
this.target = target; | ||
this.propertyName = propertyName; | ||
} | ||
|
||
/** | ||
* Bind this behavior to the source. | ||
* @param source - The source to bind to. | ||
*/ | ||
|
||
public bind(source: unknown): void { | ||
// @ts-expect-error set property on source | ||
source[this.propertyName] = this.target; | ||
} | ||
|
||
/** | ||
* Unbinds this behavior from the source. | ||
* @param source - The source to unbind from. | ||
*/ | ||
public unbind(source: unknown): void { | ||
// @ts-expect-error set property on source | ||
source[this.propertyName] = undefined; | ||
} | ||
} | ||
|
||
/** | ||
* A directive that updates a property with a reference to the source element. | ||
* Similar to RefBehavior, but sets the property back to undefined when unbound | ||
* (when the source element is removed from the DOM). | ||
* @param propertyName - The name of the property to assign the reference to. | ||
* @public | ||
*/ | ||
export function dynamicRef<T = unknown>( | ||
propertyName: keyof T & string | ||
): CaptureType<T> { | ||
return new AttachedBehaviorHTMLDirective( | ||
'nimble-dynamic-ref', | ||
DynamicRefBehavior, | ||
propertyName | ||
); | ||
} |
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 think you could collapse these two
if
s into one.