Skip to content

Commit

Permalink
Merge pull request #559 from aurelia/blur-attribute
Browse files Browse the repository at this point in the history
feat(blur): blur attribute
  • Loading branch information
fkleuver committed Aug 16, 2019
2 parents cd94c43 + a214f95 commit 9e844a8
Show file tree
Hide file tree
Showing 6 changed files with 1,126 additions and 2 deletions.
18 changes: 17 additions & 1 deletion docs/user-docs/1. getting-started/3. displaying-basic-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,22 @@ We can also use two-way data binding to communicate whether or not an element ha

When we click the input field, we see "true" printed. When we click elsewhere, it changes to "false".

### Blur

We can also use one-way data binding, from view to view model, to communicate whether an element is no longer
"focused". This is different with normal focus model of the web, which only supports "focusable" element (element with `tabindex` attribute, form fields such as `<input>` & `<select>` etc..., `<button>`, `<anchor>` with `href` attribute). Blur attribute consider any interaction inside an element as focus-maintaining action, which will not trigger blur event. It also gives ability to link elements together to determine focus-state of a group of element (think of a form submission and its confirmation dialog).

Example of reflecting the focus of a form via a property `detailFormFocused`:

```html
<form blur.bind='detailFormFocused'>
<label>Name: <input value.bind='name' /></label>
<label>Author: <input value.bind='author' /></label>
</form>
```

Then we can use `detailFormFocused` value to determine further actions.

### With

Aurelia provides a special attribute, `with`, that can be used to declare certain parts of our markup to reference properties in a child object of the view-model.
Expand Down Expand Up @@ -218,7 +234,7 @@ export class BindWith {
</template>
```

Using `with` is basically shorthand for "I'm working with properties of this object", which lets you reuse code as necessary.
Using `with` is basically shorthand for "I'm working with properties of this object", which lets you reuse code as necessary. If you are familiar with `with` keyword of JavaScript, you should be able to see some familiarities here too.

### Content Editable

Expand Down
263 changes: 263 additions & 0 deletions packages/__tests__/jit-html/blur.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import { Constructable, PLATFORM } from '@aurelia/kernel';
import { Aurelia, CustomElement } from '@aurelia/runtime';
import { Blur, Focus } from '@aurelia/runtime-html';
import { assert, eachCartesianJoin, HTMLTestContext, TestContext } from '@aurelia/testing';

describe('blur.integration.spec.ts', () => {

if (!PLATFORM.isBrowserLike) {
return;
}

interface IApp {
hasFocus: boolean;
}

describe('>> with mouse', function() {
describe('>> Basic scenarios', function() {
// Note that from-view binding are not working at the moment
// as blur has a guard to prevent unnecessary work,
// it checks if value is already false and short circuit all checks
const blurAttrs = [
// 'blur.bind=hasFocus',
'blur.two-way="hasFocus"',
// 'blur.from-view=hasFocus',
'blur="value.two-way: hasFocus"',
// 'blur="value.bind: hasFocus"',
// 'blur="value.from-view: hasFocus"'
];
const normalUsageTestCases: IBlurTestCase[] = [
{
title: (blurAttr: string) => `\n>> Case 1 \n >> Works in basic scenario with <div ${blurAttr}/>`,
template: (blurrAttr) => `<template>
<div ${blurrAttr}></div>
<button>Click me to focus</button>
</template>`,
getFocusable: 'div',
app: class App {
public hasFocus = true;
},
async assertFn(ctx, component) {
assert.equal(component.hasFocus, true, 'initial component.hasFocus');

dispatchEventWith(ctx, ctx.doc, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, 'component.hasFocus');
await waitForFrames(ctx, 1);

component.hasFocus = true;
dispatchEventWith(ctx, ctx.wnd, EVENTS.MouseDown);
assert.equal(component.hasFocus, true, 'window@mousedown -> Shoulda leave "hasFocus" alone as window is not listened to.');
await waitForFrames(ctx, 1);

component.hasFocus = true;
dispatchEventWith(ctx, ctx.doc.body, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, 'document.body@mousedown -> Shoulda set "hasFocus" to false when mousedown on doc body.');
await waitForFrames(ctx, 1);

const button = ctx.doc.querySelector('button');
component.hasFocus = true;
dispatchEventWith(ctx, button, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, '+ button@mousedown -> Shoulda set "hasFocus" to false when clicking element outside.');
}
},
{
title: (blurAttr) => `\n>> Case 2 \n >> Works in basic scenario with <input ${blurAttr}/>`,
template: (blurrAttr) => `<template>
<input ${blurrAttr}>
<button>Click me to focus</button>
</template>`,
getFocusable: 'input',
app: class App {
public hasFocus = true;
},
async assertFn(ctx, component) {
assert.equal(component.hasFocus, true, 'initial component.hasFocus');

dispatchEventWith(ctx, ctx.doc, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, 'document@mousedown -> Shoulda set "hasFocus" to false when mousedown on document.');
await waitForFrames(ctx, 1);

component.hasFocus = true;
dispatchEventWith(ctx, ctx.wnd, EVENTS.MouseDown);
assert.equal(component.hasFocus, true, 'window@mousedown -> It should have been true. Ignore interaction out of document.');
await waitForFrames(ctx, 1);

component.hasFocus = true;
dispatchEventWith(ctx, ctx.doc.body, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, 'document.body@mousedown -> Shoulda been false. Interacted inside doc, outside element.');
await waitForFrames(ctx, 1);

const button = ctx.doc.querySelector('button');
component.hasFocus = true;
dispatchEventWith(ctx, button, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, '+ button@mousedown -> Shoulda been false. Interacted outside element.');
}
}
];

eachCartesianJoin(
[blurAttrs, normalUsageTestCases],
(command, { title, template, getFocusable, app, assertFn }: IBlurTestCase) => {
it(title(command), async function() {
const { ctx, component, dispose } = setup<IApp>(
template(command),
app
);
await assertFn(ctx, component, null);
// test cases could be sharing the same context document
// so wait a bit before running the next test
await waitForFrames(ctx, 2);
await dispose();
});
}
);
});

describe.skip('Abnormal scenarios', function() {
const blurAttrs = [
// 'blur.bind=hasFocus',
'blur.two-way=hasFocus',
// 'blur.from-view=hasFocus',
'blur="value.two-way: hasFocus"',
// 'blur="value.bind: hasFocus"',
// 'blur="value.from-view: hasFocus"'
];
const abnormalCases: IBlurTestCase[] = [
{
title: (callIndex: number, blurAttr: string) => `${callIndex}. Works in abnormal scenario with <div ${blurAttr}/> binding to "child > input" focus.two-way`,
template: (blurrAttr) => `<template>
<div ${blurrAttr}></div>
<button>Click me to focus</button>
<child value.two-way="hasFocus"></child>
</template>`,
getFocusable: 'div',
app: class App {
public hasFocus = true;
},
async assertFn(ctx, component) {
const input = ctx.doc.querySelector('input');
assert.equal(input.isConnected, true);
assert.equal(input, ctx.doc.activeElement, 'child > input === doc.activeElement');
assert.equal(component.hasFocus, true, 'initial component.hasFocus');

input.blur();
dispatchEventWith(ctx, input, 'blur', false);
assert.notEqual(input, ctx.doc.activeElement, 'child > input !== doc.activeElement');
assert.equal(component.hasFocus, false, 'child > input@blur');

dispatchEventWith(ctx, ctx.doc, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, 'document@mousedown');

component.hasFocus = true;
dispatchEventWith(ctx, ctx.wnd, EVENTS.MouseDown);
assert.equal(component.hasFocus, true, 'window@mousedown');

component.hasFocus = true;
dispatchEventWith(ctx, ctx.doc.body, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, 'document.body@mousedown');

const button = ctx.doc.querySelector('button');
component.hasFocus = true;
dispatchEventWith(ctx, button, EVENTS.MouseDown);
assert.equal(component.hasFocus, false, '+ button@mousedown');

// this is quite convoluted
component.hasFocus = true;
input.focus();
dispatchEventWith(ctx, input, 'focus');
assert.equal(input, ctx.doc.activeElement, 'child > input === doc.activeElement');
assert.equal(component.hasFocus, false, 'child > input@focus');
}
},
];

eachCartesianJoin(
[blurAttrs, abnormalCases],
(command, abnormalCase, callIndex) => {
const { title, template, app, assertFn } = abnormalCase;
it(title(callIndex, command), async function() {
const { component, ctx, dispose } = setup<IApp>(
template(command),
app,
CustomElement.define(
{
name: 'child',
template: '<template><input focus.two-way="value" /></template>'
},
class Child {
public static bindables = {
value: { property: 'value', attribute: 'value' }
};
}
)
);
await assertFn(ctx, component, null);
await dispose();
});
}
);
});
});

const enum EVENTS {
MouseDown = 'mousedown',
TouchStart = 'touchstart',
PointerDown = 'pointerdown',
Focus = 'focus',
Blur = 'blur'
}

interface IBlurTestCase<T extends IApp = IApp> {
template: TemplateFn;
app: Constructable<T>;
assertFn: AssertionFn;
getFocusable: string | ((doc: Document) => HTMLElement);
title(...args: unknown[]): string;
}

function setup<T>(template: string | Node, $class: Constructable | null, ...registrations: any[]) {
const ctx = TestContext.createHTMLTestContext();
const { container, lifecycle, observerLocator } = ctx;
registrations = Array.from(new Set([...registrations, Blur, Focus]));
container.register(...registrations);
const bodyEl = ctx.doc.body;
const host = bodyEl.appendChild(ctx.createElement('app'));
const au = new Aurelia(container);
const App = CustomElement.define({ name: 'app', template }, $class);
const component = new App();

au.app({ host, component });
au.start();

return {
ctx: ctx,
au,
container,
lifecycle,
host,
component: component as T,
observerLocator,
dispose: async () => {
await au.stop().wait();
host.remove();
}
};
}

function dispatchEventWith(ctx: HTMLTestContext, target: EventTarget, name: string, bubbles = true) {
target.dispatchEvent(new ctx.Event(name, { bubbles }));
}

async function waitForFrames(ctx: HTMLTestContext, frameCount: number): Promise<void> {
while (frameCount-- > 0) {
await new Promise(r => ctx.wnd.requestAnimationFrame(r));
}
}

type TemplateFn = (focusAttrBindingCommand: string) => string;

interface AssertionFn<T extends IApp = IApp> {
// tslint:disable-next-line:callable-types
(ctx: HTMLTestContext, component: T, focusable: HTMLElement): void | Promise<void>;
}
});
Loading

0 comments on commit 9e844a8

Please sign in to comment.