@triptease/tt-multi-date-picker 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cjs/package.json +3 -0
  2. package/dist/cjs/src/TtMultiDatePicker.js +220 -0
  3. package/dist/cjs/src/TtMultiDatePicker.js.map +1 -0
  4. package/dist/cjs/src/index.js +21 -0
  5. package/dist/cjs/src/index.js.map +1 -0
  6. package/dist/cjs/src/selection-model.js +72 -0
  7. package/dist/cjs/src/selection-model.js.map +1 -0
  8. package/dist/cjs/src/styles.js +56 -0
  9. package/dist/cjs/src/styles.js.map +1 -0
  10. package/dist/cjs/src/tt-multi-date-picker.js +11 -0
  11. package/dist/cjs/src/tt-multi-date-picker.js.map +1 -0
  12. package/dist/cjs/src/types.js +3 -0
  13. package/dist/cjs/src/types.js.map +1 -0
  14. package/dist/esm/src/TtMultiDatePicker.d.ts +33 -0
  15. package/dist/esm/src/TtMultiDatePicker.js +207 -0
  16. package/dist/esm/src/TtMultiDatePicker.js.map +1 -0
  17. package/dist/esm/src/index.d.ts +2 -0
  18. package/dist/esm/src/index.js +3 -0
  19. package/dist/esm/src/index.js.map +1 -0
  20. package/dist/esm/src/selection-model.d.ts +4 -0
  21. package/dist/esm/src/selection-model.js +68 -0
  22. package/dist/esm/src/selection-model.js.map +1 -0
  23. package/dist/esm/src/styles.d.ts +1 -0
  24. package/dist/esm/src/styles.js +53 -0
  25. package/dist/esm/src/styles.js.map +1 -0
  26. package/dist/esm/src/tt-multi-date-picker.d.ts +2 -0
  27. package/dist/esm/src/tt-multi-date-picker.js +8 -0
  28. package/dist/esm/src/tt-multi-date-picker.js.map +1 -0
  29. package/dist/esm/src/types.d.ts +35 -0
  30. package/dist/esm/src/types.js +2 -0
  31. package/dist/esm/src/types.js.map +1 -0
  32. package/dist/esm/test/selection-model.test.d.ts +1 -0
  33. package/dist/esm/test/selection-model.test.js +60 -0
  34. package/dist/esm/test/selection-model.test.js.map +1 -0
  35. package/dist/esm/test/tt-multi-date-picker.test.d.ts +1 -0
  36. package/dist/esm/test/tt-multi-date-picker.test.js +173 -0
  37. package/dist/esm/test/tt-multi-date-picker.test.js.map +1 -0
  38. package/package.json +56 -0
@@ -0,0 +1,173 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { render } from 'vitest-browser-lit';
3
+ import { html } from 'lit';
4
+ import '../src/tt-multi-date-picker.js';
5
+ async function getPicker(container) {
6
+ const el = container.querySelector('tt-multi-date-picker');
7
+ if (!el)
8
+ throw new Error('tt-multi-date-picker not found');
9
+ await el.updateComplete;
10
+ return el;
11
+ }
12
+ function getCalendars(el) {
13
+ return Array.from(el.shadowRoot.querySelectorAll('tt-calendar'));
14
+ }
15
+ describe('tt-multi-date-picker', () => {
16
+ describe('rendering', () => {
17
+ it('renders 6 calendars by default', async () => {
18
+ const screen = render(html `<tt-multi-date-picker></tt-multi-date-picker>`);
19
+ const el = await getPicker(screen.container);
20
+ expect(getCalendars(el)).toHaveLength(6);
21
+ });
22
+ it('renders 3 calendars when months="3"', async () => {
23
+ const screen = render(html `<tt-multi-date-picker months="3"></tt-multi-date-picker>`);
24
+ const el = await getPicker(screen.container);
25
+ expect(getCalendars(el)).toHaveLength(3);
26
+ });
27
+ it('renders 12 calendars when months="12"', async () => {
28
+ const screen = render(html `<tt-multi-date-picker months="12"></tt-multi-date-picker>`);
29
+ const el = await getPicker(screen.container);
30
+ expect(getCalendars(el)).toHaveLength(12);
31
+ });
32
+ it('renders prev/next navigation buttons', async () => {
33
+ const screen = render(html `<tt-multi-date-picker></tt-multi-date-picker>`);
34
+ const el = await getPicker(screen.container);
35
+ const prevBtn = el.shadowRoot.querySelector('[aria-label*="Previous"]');
36
+ const nextBtn = el.shadowRoot.querySelector('[aria-label*="Next"]');
37
+ expect(prevBtn).not.toBeNull();
38
+ expect(nextBtn).not.toBeNull();
39
+ });
40
+ });
41
+ describe('selection', () => {
42
+ it('selects a date on click and emits change event', async () => {
43
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
44
+ const el = await getPicker(screen.container);
45
+ let emittedValue;
46
+ el.addEventListener('change', (e) => {
47
+ emittedValue = e.target.value;
48
+ });
49
+ const calendars = getCalendars(el);
50
+ const day = calendars[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
51
+ day.click();
52
+ await el.updateComplete;
53
+ expect(el.value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);
54
+ expect(emittedValue).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);
55
+ });
56
+ it('deselects a single date on second click', async () => {
57
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
58
+ const el = await getPicker(screen.container);
59
+ const day = getCalendars(el)[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
60
+ day.click();
61
+ await el.updateComplete;
62
+ day.click();
63
+ await el.updateComplete;
64
+ expect(el.value).toEqual([]);
65
+ });
66
+ it('creates a range with shift+click', async () => {
67
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
68
+ const el = await getPicker(screen.container);
69
+ const calendars = getCalendars(el);
70
+ const day5 = calendars[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
71
+ const day10 = calendars[0].shadowRoot.querySelector('.day[data-date="2026-03-10"]');
72
+ day5.click();
73
+ await el.updateComplete;
74
+ day10.dispatchEvent(new MouseEvent('click', { shiftKey: true, bubbles: true, composed: true }));
75
+ await el.updateComplete;
76
+ expect(el.value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-10' }]);
77
+ });
78
+ });
79
+ describe('form integration', () => {
80
+ it('submits JSON value in form', async () => {
81
+ const screen = render(html `
82
+ <form>
83
+ <tt-multi-date-picker name="dates" start-month="2026-03"></tt-multi-date-picker>
84
+ </form>
85
+ `);
86
+ const el = await getPicker(screen.container);
87
+ const day = getCalendars(el)[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
88
+ day.click();
89
+ await el.updateComplete;
90
+ const form = screen.container.querySelector('form');
91
+ const formData = new FormData(form);
92
+ const value = JSON.parse(formData.get('dates'));
93
+ expect(value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);
94
+ });
95
+ it('reports validity when required and empty', async () => {
96
+ const screen = render(html `<tt-multi-date-picker required></tt-multi-date-picker>`);
97
+ const el = await getPicker(screen.container);
98
+ expect(el.matches(':invalid')).toBe(true);
99
+ });
100
+ });
101
+ describe('footer', () => {
102
+ it('shows clear all button when there are selections', async () => {
103
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
104
+ const el = await getPicker(screen.container);
105
+ const day = getCalendars(el)[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
106
+ day.click();
107
+ await el.updateComplete;
108
+ const clearBtn = el.shadowRoot.querySelector('[data-action="clear"]');
109
+ expect(clearBtn).not.toBeNull();
110
+ });
111
+ it('clears all selections when clear button is clicked', async () => {
112
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
113
+ const el = await getPicker(screen.container);
114
+ const day = getCalendars(el)[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
115
+ day.click();
116
+ await el.updateComplete;
117
+ const clearBtn = el.shadowRoot.querySelector('[data-action="clear"]');
118
+ clearBtn.click();
119
+ await el.updateComplete;
120
+ expect(el.value).toEqual([]);
121
+ });
122
+ });
123
+ describe('keyboard navigation', () => {
124
+ it('navigates to next page with PageDown', async () => {
125
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
126
+ const el = await getPicker(screen.container);
127
+ const gridArea = el.shadowRoot.querySelector('.calendar-grid');
128
+ gridArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true }));
129
+ await el.updateComplete;
130
+ const calendars = getCalendars(el);
131
+ expect(calendars[0].getAttribute('month')).toEqual('2026-06');
132
+ });
133
+ it('navigates to previous page with PageUp', async () => {
134
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
135
+ const el = await getPicker(screen.container);
136
+ const gridArea = el.shadowRoot.querySelector('.calendar-grid');
137
+ gridArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true }));
138
+ await el.updateComplete;
139
+ const calendars = getCalendars(el);
140
+ expect(calendars[0].getAttribute('month')).toEqual('2025-12');
141
+ });
142
+ it('selects a date with Enter key', async () => {
143
+ const screen = render(html `<tt-multi-date-picker months="3" start-month="2026-03"></tt-multi-date-picker>`);
144
+ const el = await getPicker(screen.container);
145
+ const day = getCalendars(el)[0].shadowRoot.querySelector('.day[data-date="2026-03-05"]');
146
+ day.click();
147
+ await el.updateComplete;
148
+ el.value = [];
149
+ await el.updateComplete;
150
+ const gridArea = el.shadowRoot.querySelector('.calendar-grid');
151
+ gridArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
152
+ await el.updateComplete;
153
+ expect(el.value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);
154
+ });
155
+ });
156
+ describe('min/max dates', () => {
157
+ it('passes min-date and max-date to calendar instances', async () => {
158
+ const screen = render(html `
159
+ <tt-multi-date-picker
160
+ months="3"
161
+ start-month="2026-03"
162
+ min-date="2026-03-10"
163
+ max-date="2026-05-20"
164
+ ></tt-multi-date-picker>
165
+ `);
166
+ const el = await getPicker(screen.container);
167
+ const calendars = getCalendars(el);
168
+ expect(calendars[0].getAttribute('min-date')).toEqual('2026-03-10');
169
+ expect(calendars[0].getAttribute('max-date')).toEqual('2026-05-20');
170
+ });
171
+ });
172
+ });
173
+ //# sourceMappingURL=tt-multi-date-picker.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tt-multi-date-picker.test.js","sourceRoot":"","sources":["../../../test/tt-multi-date-picker.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport { render } from 'vitest-browser-lit';\nimport { html } from 'lit';\nimport '../src/tt-multi-date-picker.js';\nimport type { TtMultiDatePicker } from '../src/TtMultiDatePicker.js';\n\nasync function getPicker(container: Element): Promise<TtMultiDatePicker> {\n const el = container.querySelector<TtMultiDatePicker>('tt-multi-date-picker');\n if (!el) throw new Error('tt-multi-date-picker not found');\n await el.updateComplete;\n return el;\n}\n\nfunction getCalendars(el: TtMultiDatePicker): Element[] {\n return Array.from(el.shadowRoot!.querySelectorAll('tt-calendar'));\n}\n\ndescribe('tt-multi-date-picker', () => {\n describe('rendering', () => {\n it('renders 6 calendars by default', async () => {\n const screen = render(html`<tt-multi-date-picker></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n expect(getCalendars(el)).toHaveLength(6);\n });\n\n it('renders 3 calendars when months=\"3\"', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n expect(getCalendars(el)).toHaveLength(3);\n });\n\n it('renders 12 calendars when months=\"12\"', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"12\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n expect(getCalendars(el)).toHaveLength(12);\n });\n\n it('renders prev/next navigation buttons', async () => {\n const screen = render(html`<tt-multi-date-picker></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n const prevBtn = el.shadowRoot!.querySelector('[aria-label*=\"Previous\"]');\n const nextBtn = el.shadowRoot!.querySelector('[aria-label*=\"Next\"]');\n expect(prevBtn).not.toBeNull();\n expect(nextBtn).not.toBeNull();\n });\n });\n\n describe('selection', () => {\n it('selects a date on click and emits change event', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n let emittedValue: unknown;\n el.addEventListener('change', (e: Event) => {\n emittedValue = (e.target as TtMultiDatePicker).value;\n });\n\n const calendars = getCalendars(el);\n const day = calendars[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n day.click();\n await el.updateComplete;\n\n expect(el.value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);\n expect(emittedValue).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);\n });\n\n it('deselects a single date on second click', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const day = getCalendars(el)[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n day.click();\n await el.updateComplete;\n day.click();\n await el.updateComplete;\n\n expect(el.value).toEqual([]);\n });\n\n it('creates a range with shift+click', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const calendars = getCalendars(el);\n const day5 = calendars[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n const day10 = calendars[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-10\"]') as HTMLElement;\n\n day5.click();\n await el.updateComplete;\n day10.dispatchEvent(new MouseEvent('click', { shiftKey: true, bubbles: true, composed: true }));\n await el.updateComplete;\n\n expect(el.value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-10' }]);\n });\n });\n\n describe('form integration', () => {\n it('submits JSON value in form', async () => {\n const screen = render(html`\n <form>\n <tt-multi-date-picker name=\"dates\" start-month=\"2026-03\"></tt-multi-date-picker>\n </form>\n `);\n const el = await getPicker(screen.container);\n\n const day = getCalendars(el)[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n day.click();\n await el.updateComplete;\n\n const form = screen.container.querySelector('form')!;\n const formData = new FormData(form);\n const value = JSON.parse(formData.get('dates') as string);\n expect(value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);\n });\n\n it('reports validity when required and empty', async () => {\n const screen = render(html`<tt-multi-date-picker required></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n expect(el.matches(':invalid')).toBe(true);\n });\n });\n\n describe('footer', () => {\n it('shows clear all button when there are selections', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const day = getCalendars(el)[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n day.click();\n await el.updateComplete;\n\n const clearBtn = el.shadowRoot!.querySelector('[data-action=\"clear\"]');\n expect(clearBtn).not.toBeNull();\n });\n\n it('clears all selections when clear button is clicked', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const day = getCalendars(el)[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n day.click();\n await el.updateComplete;\n\n const clearBtn = el.shadowRoot!.querySelector('[data-action=\"clear\"]') as HTMLElement;\n clearBtn.click();\n await el.updateComplete;\n\n expect(el.value).toEqual([]);\n });\n });\n\n describe('keyboard navigation', () => {\n it('navigates to next page with PageDown', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const gridArea = el.shadowRoot!.querySelector('.calendar-grid') as HTMLElement;\n gridArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true }));\n await el.updateComplete;\n\n const calendars = getCalendars(el);\n expect(calendars[0].getAttribute('month')).toEqual('2026-06');\n });\n\n it('navigates to previous page with PageUp', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const gridArea = el.shadowRoot!.querySelector('.calendar-grid') as HTMLElement;\n gridArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true }));\n await el.updateComplete;\n\n const calendars = getCalendars(el);\n expect(calendars[0].getAttribute('month')).toEqual('2025-12');\n });\n\n it('selects a date with Enter key', async () => {\n const screen = render(html`<tt-multi-date-picker months=\"3\" start-month=\"2026-03\"></tt-multi-date-picker>`);\n const el = await getPicker(screen.container);\n\n const day = getCalendars(el)[0].shadowRoot!.querySelector('.day[data-date=\"2026-03-05\"]') as HTMLElement;\n day.click();\n await el.updateComplete;\n\n el.value = [];\n await el.updateComplete;\n\n const gridArea = el.shadowRoot!.querySelector('.calendar-grid') as HTMLElement;\n gridArea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));\n await el.updateComplete;\n\n expect(el.value).toEqual([{ startDate: '2026-03-05', endDate: '2026-03-05' }]);\n });\n });\n\n describe('min/max dates', () => {\n it('passes min-date and max-date to calendar instances', async () => {\n const screen = render(html`\n <tt-multi-date-picker\n months=\"3\"\n start-month=\"2026-03\"\n min-date=\"2026-03-10\"\n max-date=\"2026-05-20\"\n ></tt-multi-date-picker>\n `);\n const el = await getPicker(screen.container);\n const calendars = getCalendars(el);\n\n expect(calendars[0].getAttribute('min-date')).toEqual('2026-03-10');\n expect(calendars[0].getAttribute('max-date')).toEqual('2026-05-20');\n });\n });\n});\n"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@triptease/tt-multi-date-picker",
3
+ "description": "Multi-date and date range selection calendar web component",
4
+ "license": "MIT",
5
+ "author": "@triptease",
6
+ "version": "0.2.0",
7
+ "type": "module",
8
+ "main": "dist/esm/src/index.js",
9
+ "module": "dist/esm/src/index.js",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/esm/src/index.d.ts",
13
+ "import": "./dist/esm/src/index.js",
14
+ "require": "./dist/cjs/src/index.js"
15
+ },
16
+ "./types": {
17
+ "types": "./dist/esm/src/types.d.ts",
18
+ "import": "./dist/esm/src/types.js",
19
+ "require": "./dist/cjs/src/types.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/esm",
24
+ "dist/cjs"
25
+ ],
26
+ "scripts": {
27
+ "analyze": "cem analyze --litelement",
28
+ "build": "yarn build:node && yarn build:web && yarn analyze --exclude dist",
29
+ "build:esm": "tsc",
30
+ "build:cjs": "tsc -p tsconfig.cjs.json && node ../../scripts/create-cjs-package.mjs",
31
+ "build:node": "yarn build:esm && yarn build:cjs",
32
+ "build:node:watch": "tsc --watch",
33
+ "build:web": "node ../../scripts/esbuild.mjs",
34
+ "prepublish": "tsc && yarn analyze --exclude dist",
35
+ "test": "yarn playwright install && vitest run --config=./vitest.browser.config.mjs",
36
+ "test:watch": "vitest --config=./vitest.browser.config.mjs"
37
+ },
38
+ "dependencies": {
39
+ "@triptease/icons": "1.4.1",
40
+ "@triptease/stylesheet": "2.1.8",
41
+ "@triptease/tt-calendar": "6.2.0",
42
+ "lit": "^3.1.4",
43
+ "luxon": "^3.6.1"
44
+ },
45
+ "devDependencies": {
46
+ "@custom-elements-manifest/analyzer": "^0.10.3",
47
+ "@types/luxon": "^3.6.2",
48
+ "playwright": "^1.57.0",
49
+ "tslib": "^2.6.3",
50
+ "typescript": "^5.5.3"
51
+ },
52
+ "customElements": "custom-elements.json",
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }