Skip to content

Commit

Permalink
Migrate datetimepicker / chooser to Stimulus DateController
Browse files Browse the repository at this point in the history
- Avoid all need for Telepath widgets for datetimepicker, remove all inline script usage, replace with a single Stimulus controller 'w-date'
- Replaces 'soft' focus approach (which just did not focus on datetimepicker at all) with a more accessible approach that shifts focus to parent element
- Ensure that Block variants use the same HTML (which will now include autocomplete off)
- Ensure that options cannot override core functionality (hideCurrent, change event dispatch)
- Split out date compare into own util (with tests)
- Add unit test functionality to cover all datetimepicker usage scenarios
- Remove the need for a dedicated datetimepicker JS import
- Support existing wagtailConfig global usage
- Find another test case to check the media inheritance approach as no longer needed for Admin date widgets
- Fixes #10260
- Fixes #9406
  • Loading branch information
lb- committed Mar 24, 2023
1 parent 2566d4f commit bed4a5e
Show file tree
Hide file tree
Showing 16 changed files with 476 additions and 573 deletions.
8 changes: 3 additions & 5 deletions client/src/components/StreamField/blocks/BaseSequenceBlock.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class BaseSequenceChild extends EventEmitter {
? `data-contentpath="${h(this.id)}"`
: 'data-contentpath-disabled'
}>
<input type="hidden" name="${this.prefix}-deleted" value="">
<input type="hidden" name="${this.prefix}-deleted" value="">
<input type="hidden" name="${this.prefix}-order" value="${index}">
<input type="hidden" name="${this.prefix}-type" value="${h(
this.type || '',
Expand Down Expand Up @@ -252,13 +252,11 @@ export class BaseSequenceChild extends EventEmitter {
setTimeout(() => {
dom.slideDown();
if (focus) {
// focus this field if we can do so without obtrusive UI behaviour
this.block.focus({ soft: true });
this.block.focus();
}
}, 10);
} else if (focus) {
// focus this field if we can do so without obtrusive UI behaviour
this.block.focus({ soft: true });
this.block.focus();
}
}

Expand Down
198 changes: 198 additions & 0 deletions client/src/controllers/DateController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import $ from 'jquery';

import { Application } from '@hotwired/stimulus';
import { DateController } from './DateController';

jest.useFakeTimers();

describe('DateController', () => {
window.wagtailConfig = {
STRINGS: { MONTHS: ['JAN'], WEEKDAYS: ['MON'], WEEKDAYS_SHORT: ['M'] },
};

const datetimepickerMock = jest.fn();
let application;

beforeEach(() => {
jest.clearAllMocks();

$.fn.datetimepicker = datetimepickerMock;
$.datetimepicker = datetimepickerMock;
datetimepickerMock.defaults = { i18n: {} };
datetimepickerMock.setLocale = jest.fn();

application?.unload('w-date');
application?.stop();
});

it('should set the default translations based on global config when controller is registered', () => {
expect(datetimepickerMock.defaults.i18n).toEqual({});
expect(datetimepickerMock.setLocale).not.toHaveBeenCalled();

application = Application.start();
application.register('w-date', DateController);

expect(datetimepickerMock.defaults.i18n).toHaveProperty(
'wagtail_custom_locale',
{ months: ['JAN'], dayOfWeek: ['MON'], dayOfWeekShort: ['M'] },
);
expect(datetimepickerMock.setLocale).toHaveBeenCalledWith(
'wagtail_custom_locale',
);
});

it('should create a date picker only by default', async () => {
document.body.innerHTML = `
<main>
<input id="date" type="text" data-controller="w-date" value="2020-01-01" />
<ul id="datepicker-overlay" class="_MOCK_ xdsoft_datepicker">
<li class="A xdsoft_current xdsoft_today"></li>
<li class="B xdsoft_current"></li>
</ul>
</main>
`;

application = Application.start();
application.register('w-date', DateController);

// await the next render
await Promise.resolve();

// ensure the jQuery plugin gets correctly called
expect(datetimepickerMock).toHaveBeenLastCalledWith({
closeOnDateSelect: true,
format: 'Y-m-d',
onChangeDateTime: expect.any(Function),
onGenerate: expect.any(Function),
scrollInput: false,
timepicker: false,
});

// check the hide current functionality works as expected
datetimepickerMock.mock.calls[0][0].onGenerate.apply(
[document.getElementById('datepicker-overlay')],
[new Date(), document.getElementById('date')],
);

expect(
document.querySelectorAll('.A.xdsoft_current.xdsoft_today').length,
).toBe(1);
expect(document.querySelectorAll('.B.xdsoft_current').length).toBe(0);

// ensure that the jQuery plugin gets removed on disconnect
document.getElementById('date').remove();

await Promise.resolve();

expect(datetimepickerMock).toHaveBeenLastCalledWith('destroy');
});

it('should set up the ability to trigger a non-obtrusive focus shortly after creation', async () => {
document.body.innerHTML = `
<section id="section">
<input id="date" type="text" data-controller="w-date" />
<button type="button">A target</button>
</section>
`;

application = Application.start();
application.register('w-date', DateController);

// await the next render
await Promise.resolve();

const dateField = document.getElementById('date');

dateField.focus();

jest.runAllTimers();

const section = document.getElementById('section');

expect(document.activeElement).toBe(section);
expect(section.getAttribute('tabindex')).toBe('-1');
expect(datetimepickerMock).toHaveBeenLastCalledWith('hide');

// remove tabindex once something else gains focus
document.querySelector('button').focus();
expect(section.getAttribute('tabindex')).toBeNull();

// on focus triggered after threshold, should act as normal
dateField.focus();
expect(document.activeElement).toBe(dateField);
});

it('should support a datetime picker mode', async () => {
document.body.innerHTML = `
<section id="section">
<input id="date" type="text" data-controller="w-date" data-w-date-mode-value="datetime" />
</section>
`;

application = Application.start();
application.register('w-date', DateController);

await Promise.resolve();

expect(datetimepickerMock).toHaveBeenLastCalledWith({
closeOnDateSelect: true,
format: 'Y-m-d H:i',
onChangeDateTime: expect.any(Function),
onGenerate: expect.any(Function),
scrollInput: false,
});
});

it('should support a time picker mode', async () => {
document.body.innerHTML = `
<section id="section">
<input id="date" type="text" data-controller="w-date" data-w-date-mode-value="time" />
</section>
`;

application = Application.start();
application.register('w-date', DateController);

await Promise.resolve();

expect(datetimepickerMock).toHaveBeenLastCalledWith({
closeOnDateSelect: true,
datepicker: false,
format: 'H:i',
onChangeDateTime: expect.any(Function),
onGenerate: expect.any(Function),
scrollInput: false,
});
});

it('should support additional options passed in', async () => {
document.body.innerHTML = `
<section id="section">
<input id="date" type="text" data-controller="w-date" data-w-date-mode-value="date" />
</section>
`;

document.getElementById('date').dataset.wDateOptionsValue = JSON.stringify({
closeOnDateSelect: false,
format: 'AA BB YY',
onGenerate: null, // should be ignored
scrollInput: true,
xOther: true,
});

application = Application.start();
application.register('w-date', DateController);

await Promise.resolve();

expect(datetimepickerMock).toHaveBeenLastCalledWith({
closeOnDateSelect: false,
format: 'AA BB YY',
onChangeDateTime: expect.any(Function),
onGenerate: expect.any(Function), // not null
scrollInput: true,
timepicker: false,
xOther: true,
});
});
});

0 comments on commit bed4a5e

Please sign in to comment.