Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/docs/mover.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ very inconvenient for the virtualized lists when the scrolling causes more items
With `visibilityAware` we can alter that behaviour to be able to Tab to the first visible
element instead of the first rendered one. Enables `trackState` internally.

### disableHomeEndKeys?: _boolean_

As the name says, we can disable Home/End keys if needed.

## Examples

[See a few Mover examples in the Storybook](https://tabster.io/storybook/?path=/story/mover).
43 changes: 22 additions & 21 deletions src/Mover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,32 +285,40 @@ export class Mover
: undefined;
}

const { memorizeCurrent, visibilityAware } = this._props;
const { memorizeCurrent, visibilityAware, hasDefault } = this._props;
const moverElement = this.getElement();

if (
moverElement &&
(memorizeCurrent || visibilityAware) &&
(memorizeCurrent || visibilityAware || hasDefault) &&
(!moverElement.contains(state.from) ||
(
state.from as HTMLElementWithDummyContainer
).__tabsterDummyContainer?.get() === moverElement)
) {
let found: HTMLElement | undefined | null;

if (memorizeCurrent) {
const current = this._current?.get();

if (current && state.acceptCondition(current)) {
state.found = true;
state.foundElement = current;
state.lastToIgnore = moverElement;
return NodeFilter.FILTER_ACCEPT;
found = current;
}
}

if (visibilityAware) {
const found = this._tabster.focusable.findElement({
if (!found && hasDefault) {
found = this._tabster.focusable.findDefault({
container: moverElement,
ignoreUncontrolled: true,
useActiveModalizer: true,
});
}

if (!found && visibilityAware) {
found = this._tabster.focusable.findElement({
container: moverElement,
ignoreUncontrolled: true,
useActiveModalizer: true,
isBackward: state.isBackward,
acceptCondition: (el) => {
const id = getElementUId(this._win, el);
Expand All @@ -329,13 +337,13 @@ export class Mover
);
},
});
}

if (found) {
state.found = true;
state.foundElement = found;
state.lastToIgnore = moverElement;
return NodeFilter.FILTER_ACCEPT;
}
if (found) {
state.found = true;
state.foundElement = found;
state.lastToIgnore = moverElement;
return NodeFilter.FILTER_ACCEPT;
}
}

Expand Down Expand Up @@ -823,13 +831,6 @@ export class MoverAPI implements Types.MoverAPI {
focusedElementX2 = Math.floor(focusedElementRect.right);
}

if (
moverProps.disableHomeEndKeys &&
(keyCode === Keys.Home || keyCode === Keys.End)
) {
return;
}

if (ctx.isRtl) {
if (keyCode === Keys.Right) {
keyCode = Keys.Left;
Expand Down
7 changes: 6 additions & 1 deletion src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ export type FindDefaultProps = Pick<
| "modalizerId"
| "includeProgrammaticallyFocusable"
| "useActiveModalizer"
| "ignoreUncontrolled"
| "ignoreAccessibiliy"
>;

Expand Down Expand Up @@ -595,7 +596,11 @@ export interface MoverProps {
* element in DOM) when tabbing from outside of the mover.
*/
visibilityAware?: Visibility;
disableHomeEndKeys?: boolean;
/**
* When true, Mover will try to locate a focusable with Focusable.isDefault
* property as a prioritized element to focus.
*/
hasDefault?: boolean;
}

export type MoverEvent = TabsterEventWithDetails<MoverElementState>;
Expand Down
8 changes: 4 additions & 4 deletions stories/Mover/Mover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export type MoverProps = TabsterTypes.MoverProps;
export const createBasicMover = ({
cyclic,
direction,
disableHomeEndKeys,
memorizeCurrent,
tabbable,
trackState,
visibilityAware,
hasDefault,
}: MoverProps) => {
console.log(direction);

Expand All @@ -34,11 +34,11 @@ export const createBasicMover = ({
mover: {
cyclic,
direction,
disableHomeEndKeys,
memorizeCurrent,
tabbable,
trackState,
visibilityAware,
hasDefault,
},
},
true
Expand All @@ -52,22 +52,22 @@ export const createBasicMover = ({
export const createTableMover = ({
cyclic,
direction,
disableHomeEndKeys,
memorizeCurrent,
tabbable,
trackState,
visibilityAware,
hasDefault,
}: MoverProps) => {
const attr = getTabsterAttribute(
{
mover: {
cyclic,
direction,
disableHomeEndKeys,
memorizeCurrent,
tabbable,
trackState,
visibilityAware,
hasDefault,
},
},
true
Expand Down
100 changes: 100 additions & 0 deletions tests/Mover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2011,3 +2011,103 @@ describe("Adjacent Movers", () => {
}
);
});

describe("Mover with default element", () => {
beforeEach(async () => {
await BroTest.bootstrapTabsterPage({ mover: true });
});

it("should focus default focusable when tabbing from outside", async () => {
await new BroTest.BroTest(
(
<div {...getTabsterAttribute({ root: {} })}>
<button>Button1</button>
<div
{...getTabsterAttribute({
mover: { hasDefault: true },
})}
>
<button>Button2</button>
<button>Button3</button>
<button
{...getTabsterAttribute({
focusable: { isDefault: true },
})}
>
Button4
</button>
<button>Button5</button>
</div>
<button>Button6</button>
</div>
)
)
.pressTab()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button1");
})
.pressTab()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button4");
})
.pressUp()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button3");
})
.pressTab()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button6");
})
.pressTab(true)
.activeElement((el) => {
expect(el?.textContent).toEqual("Button4");
});
});

it("should focus memorized element or default focusable when tabbing from outside", async () => {
await new BroTest.BroTest(
(
<div {...getTabsterAttribute({ root: {} })}>
<button>Button1</button>
<div
{...getTabsterAttribute({
mover: { hasDefault: true, memorizeCurrent: true },
})}
>
<button>Button2</button>
<button>Button3</button>
<button
{...getTabsterAttribute({
focusable: { isDefault: true },
})}
>
Button4
</button>
<button>Button5</button>
</div>
<button>Button6</button>
</div>
)
)
.pressTab()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button1");
})
.pressTab()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button4");
})
.pressUp()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button3");
})
.pressTab()
.activeElement((el) => {
expect(el?.textContent).toEqual("Button6");
})
.pressTab(true)
.activeElement((el) => {
expect(el?.textContent).toEqual("Button3");
});
});
});