From 0a28aa7d93685fcbfc29532136d1917719077b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Bla=C5=BEek?= Date: Thu, 2 Jul 2026 13:31:13 +0200 Subject: [PATCH 01/10] feat: migrate to Winduum 3.0.0-next.7; refactor components to align with new APIs --- CHANGELOG.md | 13 +++ README.md | 2 +- components/button/index.js | 3 + components/carousel/index.js | 79 ++++++++--------- components/control/index.js | 17 +++- components/details/index.js | 38 --------- components/dialog/index.js | 23 ----- components/dialog/readme.md | 72 ---------------- components/drawer/index.js | 107 +++++++----------------- components/field/index.js | 13 +++ components/{details => field}/readme.md | 10 +-- components/form/index.js | 6 +- components/image/readme.md | 26 ++++++ components/popover/index.js | 56 ++++++++----- components/range/index.js | 11 +-- components/toaster/index.js | 11 +++ package-lock.json | 12 +-- package.json | 4 +- utilities/invoke/readme.md | 4 +- 19 files changed, 203 insertions(+), 304 deletions(-) delete mode 100644 components/details/index.js delete mode 100644 components/dialog/index.js delete mode 100644 components/dialog/readme.md create mode 100644 components/field/index.js rename components/{details => field}/readme.md (52%) create mode 100644 components/image/readme.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c7f4df3..8d5c321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 3.0.0-next.0 (2026-07-02) +* feat!: updated components to Winduum `3.0.0-next.7` (aligned with `winduum-elements`) +* feat!: carousel rewritten to new API (`scrollBy`, `toggleScrollState`, `setSnappedAttribute`, `scrollToMarker`) — `pagination`, `counter` and `progress` targets replaced by `markerGroup`/`marker` targets and `vertical` value +* feat!: drawer rewritten to new dialog-based API (`drawerEvents`, `drawerObserver`, `showDrawer`) — `dialog` value replaced by boolean `modal` value +* feat!: popover rewritten to native Popover API with anchor positioning and `@floating-ui` fallback (`autoUpdate`, `placement` values) +* feat!: removed dialog and details components (handled natively in Winduum 3) +* feat!: form component now only validates on submit — field validation moved to the new field component +* feat: added field component +* feat: control component now toggles `data-active` attribute on change +* feat: button component now shows ripple on click automatically +* feat: toaster component now uses `toasterObserver` +* fix: range component no longer calls `setTrackProperty` (handled internally by `setValue`) + # 2.0.10, 2.0.11 (2025-04-30) * feat: added carousel component diff --git a/README.md b/README.md index d7a7f51..d64d9ca 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ Ready to use components and utilities for Stimulus { "@hotwired/stimulus": "^3", "@newlogic-digital/utils-js": "^1", - "winduum": "^2" + "winduum": "^3" } ``` diff --git a/components/button/index.js b/components/button/index.js index 4ec5a4e..c7f377a 100644 --- a/components/button/index.js +++ b/components/button/index.js @@ -1,4 +1,5 @@ import { Controller } from '@hotwired/stimulus' +import { dataset } from '@newlogic-digital/utils-js' export class Button extends Controller { static values = { @@ -30,6 +31,8 @@ export class Button extends Controller { this.loadingValue && this.observer.observe(this.element, { attributeFilter: [this.loadingAttribute], }) + + dataset(this.element, 'action').add(`click->${this.identifier}#ripple`) } disconnect() { diff --git a/components/carousel/index.js b/components/carousel/index.js index e2af46d..fa01c6e 100644 --- a/components/carousel/index.js +++ b/components/carousel/index.js @@ -1,67 +1,60 @@ import { Controller } from '@hotwired/stimulus' +import { dataset } from '@newlogic-digital/utils-js' export class Carousel extends Controller { - static targets = ['content', 'counterMin', 'counterMax', 'pagination', 'progress', 'prev', 'next'] + static targets = ['content', 'markerGroup', 'marker', 'prev', 'next'] - paginationItemContent = '
' + static values = { + vertical: Boolean, + } async connect() { - await this.scroll() + const { setSnappedAttribute, toggleScrollState } = await import('winduum/src/components/carousel/index.js') + + this.abortController = new AbortController() + const signal = this.abortController.signal - if (this.hasPaginationTarget) { - const { paginationCarousel } = await import('winduum/src/components/carousel/index.js') + this.contentTarget.addEventListener('scrollsnapchanging', (event) => { + setSnappedAttribute(this.contentTarget, event.snapTargetInline ?? event.snapTargetBlock, this.hasMarkerGroupTarget ? this.markerGroupTarget : null) + }, { signal }) - paginationCarousel(this.contentTarget, { - element: this.paginationTarget, - itemContent: this.paginationItemContent, + this.contentTarget.addEventListener('scroll', () => { + toggleScrollState(this.contentTarget, { + prevElement: this.hasPrevTarget ? this.prevTarget : null, + nextElement: this.hasNextTarget ? this.nextTarget : null, + vertical: this.verticalValue, }) - } + }, { signal }) } - async scroll() { - const { scrollCarousel } = await import('winduum/src/components/carousel/index.js') - - this.toggleScrollAttributes() + disconnect() { + this.abortController?.abort() + } - scrollCarousel(this.contentTarget, { - counterMinElement: this.hasCounterMinTarget && this.counterMinTarget, - counterMaxElement: this.hasCounterMaxTarget && this.counterMaxTarget, - progressElement: this.hasProgressTarget && this.progressTarget, - pagination: { - element: this.hasPaginationTarget && this.paginationTarget, - }, - }) + markerTargetConnected(element) { + dataset(element, 'action').add(`click->${this.identifier}#scrollToMarker:prevent`) } - toggleScrollAttributes() { - const scrollStart = this.contentTarget.scrollLeft <= 0 - const scrollEnd = this.contentTarget.scrollLeft >= this.contentTarget.scrollWidth - this.contentTarget.clientWidth - const scrollNone = this.contentTarget.scrollWidth - this.contentTarget.clientWidth === 0 + async scrollToMarker({ currentTarget }) { + const { scrollToMarker } = await import('winduum/src/components/carousel/index.js') + + scrollToMarker(this.contentTarget, currentTarget, this.markerGroupTarget, this.verticalValue ? { block: 'start' } : {}) + } - if (this.hasPrevTarget && this.hasNextTarget) { - this.prevTarget.disabled = scrollStart - this.nextTarget.disabled = scrollEnd - } + async scroll(direction) { + const { scrollBy } = await import('winduum/src/components/carousel/index.js') - this.element.toggleAttribute('data-scroll-start', scrollStart) - this.element.toggleAttribute('data-scroll-end', scrollEnd) - this.element.toggleAttribute('data-scroll-none', scrollNone) + scrollBy(this.contentTarget, { + direction, + vertical: this.verticalValue, + }) } scrollPrev() { - this.contentTarget.scroll({ left: this.contentTarget.scrollLeft - this.contentTarget.children[0].offsetWidth }) + this.scroll(-1) } scrollNext() { - this.contentTarget.scroll({ left: this.contentTarget.scrollLeft + this.contentTarget.children[0].offsetWidth }) - } - - async scrollTo({ currentTarget }) { - const { scrollTo } = await import('winduum/src/components/carousel/index.js') - - const siblingElements = [...currentTarget.parentElement.children] - const index = siblingElements.indexOf(currentTarget) - - scrollTo(this.contentTarget, index) + this.scroll(1) } } diff --git a/components/control/index.js b/components/control/index.js index 8229eb3..5ca108d 100644 --- a/components/control/index.js +++ b/components/control/index.js @@ -1,7 +1,22 @@ import { Controller } from '@hotwired/stimulus' -import { dispatchCustomEvent } from '@newlogic-digital/utils-js' +import { dataset, dispatchCustomEvent } from '@newlogic-digital/utils-js' export class Control extends Controller { + activeAttribute = 'data-active' + + connect() { + this.toggleActiveAttribute() + dataset(this.element, 'action').add(`change->${this.identifier}#toggleActiveAttribute`) + } + + toggleActiveAttribute() { + const telCountryCode = this.element.querySelector('[autocomplete="tel-country-code"]') + + if (telCountryCode) telCountryCode.dataset.value = telCountryCode.value + + this.element.toggleAttribute(this.activeAttribute, !!this.element.querySelector('input:not([type="hidden"]), textarea, select')?.value) + } + stepUp() { this.element.querySelector('input').stepUp() dispatchCustomEvent(this.element.querySelector('input')) diff --git a/components/details/index.js b/components/details/index.js deleted file mode 100644 index 5deceb2..0000000 --- a/components/details/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import { Controller } from '@hotwired/stimulus' - -export class Details extends Controller { - async show({ currentTarget, params }) { - const { showDetails } = await import('winduum/src/components/details/index.js') - - await showDetails(currentTarget, params) - } - - async close({ currentTarget, params }) { - const { closeDetails } = await import('winduum/src/components/details/index.js') - - await closeDetails(currentTarget, params) - } - - async toggle({ currentTarget, params }) { - const { toggleDetails } = await import('winduum/src/components/details/index.js') - - this.closeSiblings({ params }) - - await toggleDetails(currentTarget, params) - } - - closeSiblings({ params }) { - if (!this.element.dataset.name) return - - document.querySelectorAll(`details[data-name="${this.element.dataset.name}"]`).forEach((currentTarget) => { - if (currentTarget !== this.element) this.close({ currentTarget, params }) - }) - } - - connect() { - if (this.element.name) { - this.element.dataset.name = this.element.name - this.element.removeAttribute('name') - } - } -} diff --git a/components/dialog/index.js b/components/dialog/index.js deleted file mode 100644 index 289467c..0000000 --- a/components/dialog/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Controller } from '@hotwired/stimulus' - -export class Dialog extends Controller { - static values = { - params: Object, - } - - connect() { - this.dispatch('connect') - } - - async show(event) { - const { showDialog } = await import('winduum/src/components/dialog/index.js') - - await showDialog(this.element, this.hasParamsValue ? this.paramsValue : event?.params) - } - - async close(event) { - const { closeDialog } = await import('winduum/src/components/dialog/index.js') - - await closeDialog(this.element, this.hasParamsValue ? this.paramsValue : event?.params) - } -} diff --git a/components/dialog/readme.md b/components/dialog/readme.md deleted file mode 100644 index eb2af45..0000000 --- a/components/dialog/readme.md +++ /dev/null @@ -1,72 +0,0 @@ -# [Dialog](https://winduum.dev/docs/components/dialog.html) - -## Installation -```shell -npm i winduum-stimulus -``` - -```js -import { Application } from '@hotwired/stimulus' -import { Dialog } from 'winduum-stimulus/components/dialog/index.js' - -const application = Application.start() - -application.register('x-dialog', Dialog) -``` - -### Local imports -By default, imports are directly from `npm` so you can leverage updates. -Alternatively, you can also copy and paste the code from this directory to your project and remap the imports to local. - -```js -import { Dialog } from '@/components/ui/dialog/index.js' -``` - -### Docs -Visit [docs](https://winduum.dev/docs/components/dialog.html) to learn more about JavaScript API and see usage examples. - -## Actions - -Controller is present directly on the `dialog` element, so you need to use [Invoke](https://github.com/winduum/winduum-stimulus/tree/main/utilities/invoke) action to invoke an action outside the controller. - -### show -Show a dialog via [showDialog](https://winduum.dev/docs/components/dialog.html#showdialog) method.
-You can add any params from [options](https://winduum.dev/docs/components/dialog.html#showoptions) via [Invoke](https://github.com/winduum/winduum-stimulus/tree/main/utilities/invoke) params or via `data-x-dialog-params-value` on `x-dialog`. - -#### Example - -```html - - - -
- Dialog content -
-
- -``` - -### close -Close a dialog via [closeDialog](https://winduum.dev/docs/components/dialog.html#closedialog) method.
-You can add any params from [options](https://winduum.dev/docs/components/dialog.html#closeoptions) via [Action](https://stimulus.hotwired.dev/reference/actions#action-parameters) params or via `data-x-dialog-params-value` on `x-dialog`. - -#### Example - -```html - - -
- -
-
- -``` diff --git a/components/drawer/index.js b/components/drawer/index.js index f48d406..6fabaaa 100644 --- a/components/drawer/index.js +++ b/components/drawer/index.js @@ -8,99 +8,56 @@ export class Drawer extends Controller { type: String, default: 'left', }, - dialog: { - type: String, - default: 'modal', + modal: { + type: Boolean, + default: true, }, } - async scroll({ target }) { - const { scrollDrawer } = await import('winduum/src/components/drawer/index.js') - - const bottomTop = { - snapClass: 'snap-y snap-mandatory', - scrollSize: target.scrollHeight - target.clientHeight, - scrollDirection: target.scrollTop, - } - - const rightBottom = { - scrollClose: 0, - opacityRatio: 0, - } + connect() { + this.abortController = new AbortController() - const placement = { - right: { - ...rightBottom, - scrollOpen: target.scrollWidth - target.clientWidth, - }, - bottom: { - ...rightBottom, - ...bottomTop, - scrollOpen: target.scrollHeight - target.clientHeight, - }, - top: { - ...bottomTop, - scrollOpen: 0, - scrollClose: target.scrollHeight - target.clientHeight, - }, - } - - await scrollDrawer(target, placement[this.placementValue]) + this.element.addEventListener('close', () => { + if (this.triggerElement) this.triggerElement.ariaExpanded = false + }, { signal: this.abortController.signal }) } - async show() { - const { showDrawer, scrollInitDrawer } = await import('winduum/src/components/drawer/index.js') + disconnect() { + this.abortController?.abort() + } - if (this.dialogValue === 'modal') { - this.element.showModal() - } - else if (this.dialogValue === 'non-modal') { - this.element.show() - } + async contentTargetConnected() { + const { drawerObserver, drawerEvents } = await import('winduum/src/components/drawer/index.js') - const [distance, distanceClosed, direction] = { - right: [this.element.scrollWidth, 0, 'left'], - bottom: [this.element.scrollHeight, 0, 'top'], - top: [0, this.element.scrollHeight, 'top'], - }[this.placementValue] ?? [] + drawerEvents(this.element, this.contentTarget, this.placementValue, this.abortController.signal) - await scrollInitDrawer(this.element, distanceClosed, direction) + this.observer = drawerObserver(this.element, this.placementValue) + this.observer.observe(this.contentTarget) + } - await showDrawer(this.element, distance, direction) + contentTargetDisconnected() { + this.observer?.disconnect() } - async close() { - const { closeDrawer } = await import('winduum/src/components/drawer/index.js') + async show({ currentTarget }) { + const { showDrawer } = await import('winduum/src/components/drawer/index.js') - const [distance, direction] = { - right: [0, 'left'], - bottom: [0, 'top'], - top: [this.element.scrollHeight, 'top'], - }[this.placementValue] ?? [] + this.triggerElement = currentTarget - await closeDrawer(this.element, distance, direction) + if (this.element.open) return + if (this.modalValue) this.element.showModal() + else this.element.show() - if (this.triggerElement) this.triggerElement.ariaExpanded = false + currentTarget.ariaExpanded = true + showDrawer(this.element.firstElementChild, this.placementValue) } - async dismiss({ target }) { - if (!this.element.open) return - - if (!this.contentTarget.contains(target) && !this.contentTarget.isEqualNode(target)) { - await this.close() - } + close() { + this.element.close() } - async toggle({ currentTarget }) { - this.triggerElement = currentTarget - - if (this.element.inert) { - currentTarget.ariaExpanded = true - this.show() - } - else { - currentTarget.ariaExpanded = false - this.close() - } + toggle(event) { + if (this.element.open) this.close() + else this.show(event) } } diff --git a/components/field/index.js b/components/field/index.js new file mode 100644 index 0000000..d298328 --- /dev/null +++ b/components/field/index.js @@ -0,0 +1,13 @@ +import { Controller } from '@hotwired/stimulus' +import { validateField } from 'winduum/src/components/form/index.js' +import { dataset } from '@newlogic-digital/utils-js' + +export class Field extends Controller { + connect() { + dataset(this.element, 'action').add(`change->${this.identifier}#validateField`) + } + + validateField({ params }) { + validateField(this.element, params) + } +} diff --git a/components/details/readme.md b/components/field/readme.md similarity index 52% rename from components/details/readme.md rename to components/field/readme.md index cfb5af8..b96a96d 100644 --- a/components/details/readme.md +++ b/components/field/readme.md @@ -1,4 +1,4 @@ -# [Details](https://winduum.dev/docs/components/details.html) +# [Field](https://winduum.dev/docs/components/field.html) ## Installation ```shell @@ -7,11 +7,11 @@ npm i winduum-stimulus ```js import { Application } from '@hotwired/stimulus' -import { Details } from 'winduum-stimulus/components/details/index.js' +import { Field } from 'winduum-stimulus/components/field/index.js' const application = Application.start() -application.register('x-details', Details) +application.register('x-field', Field) ``` ### Local imports @@ -19,8 +19,8 @@ By default, imports are directly from `npm` so you can leverage updates. Alternatively, you can also copy and paste the code from this directory to your project and remap the imports to local. ```js -import { Details } from '@/components/ui/details/index.js' +import { Field } from '@/components/ui/field/index.js' ``` ### Docs -Visit [docs](https://winduum.dev/docs/components/details.html) to learn more about JavaScript API and see usage examples. +Visit [docs](https://winduum.dev/docs/components/field.html) to learn more about JavaScript API and see usage examples. diff --git a/components/form/index.js b/components/form/index.js index 82c44ef..afac183 100644 --- a/components/form/index.js +++ b/components/form/index.js @@ -1,5 +1,5 @@ import { Controller } from '@hotwired/stimulus' -import { validateForm, validateField } from 'winduum/src/components/form/index.js' +import { validateForm } from 'winduum/src/components/form/index.js' import { dataset } from '@newlogic-digital/utils-js' export class Form extends Controller { @@ -11,8 +11,4 @@ export class Form extends Controller { validateForm(event) { validateForm(event, event?.params) } - - validateField({ currentTarget, params }) { - validateField(currentTarget, params) - } } diff --git a/components/image/readme.md b/components/image/readme.md new file mode 100644 index 0000000..03aab32 --- /dev/null +++ b/components/image/readme.md @@ -0,0 +1,26 @@ +# [Image](https://winduum.dev/docs/components/image.html) + +## Installation +```shell +npm i winduum-stimulus +``` + +```js +import { Application } from '@hotwired/stimulus' +import { Image } from 'winduum-stimulus/components/image/index.js' + +const application = Application.start() + +application.register('x-image', Image) +``` + +### Local imports +By default, imports are directly from `npm` so you can leverage updates. +Alternatively, you can also copy and paste the code from this directory to your project and remap the imports to local. + +```js +import { Image } from '@/components/ui/image/index.js' +``` + +### Docs +Visit [docs](https://winduum.dev/docs/components/image.html) to learn more about JavaScript API and see usage examples. diff --git a/components/popover/index.js b/components/popover/index.js index 1b1b73f..3637968 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -1,45 +1,57 @@ import { Controller } from '@hotwired/stimulus' import { dataset } from '@newlogic-digital/utils-js' +import { supportsAnchoredContainer, supportsAnchor } from 'winduum/src/common.js' export class Popover extends Controller { static targets = ['action'] + static values = { - params: Object, + autoUpdate: Boolean, + placement: String, } + open = false + connect() { - this.dispatch('connect') + this.abortController = new AbortController() } - async toggle({ currentTarget }) { - const { togglePopover } = await import('winduum/src/components/popover/index.js') - - this.popoverTarget = document.getElementById(currentTarget.getAttribute('popovertarget')) - - await togglePopover(currentTarget, this.hasParamsValue ? this.paramsValue : arguments[0]?.params) + disconnect() { + this.abortController?.abort() } - async hide() { - if (this.actionTarget.ariaExpanded !== 'true') return + actionTargetConnected() { + this.popoverElement = document.getElementById(this.actionTarget.getAttribute('popovertarget')) - const { hidePopover } = await import('winduum/src/components/popover/index.js') + this.popoverElement?.addEventListener('toggle', (event) => { + this.open = event.newState === 'open' + if (this.actionTarget.ariaExpanded) this.actionTarget.ariaExpanded = this.open + }, { signal: this.abortController.signal }) - await hidePopover(this.actionTarget) + dataset(this.actionTarget, 'action').add( + `click->${this.identifier}#${this.actionTarget.getAttribute('popovertargetaction') ?? 'toggle'}:prevent`, + ) } - async dismiss({ target }) { - if (this.actionTarget.ariaExpanded !== 'true') return + async show() { + if ((this.autoUpdateValue && !supportsAnchoredContainer) || !supportsAnchor) { + const { autoUpdatePopover } = await import('winduum/src/components/popover/index.js') - if (!this.popoverTarget.contains(target) && !this.actionTarget.isEqualNode(target) && this.actionTarget.ariaExpanded === 'true') { - await this.hide() + this.cleanup = await autoUpdatePopover(this.actionTarget, this.popoverElement, this.placementValue, this.autoUpdateValue) } + + this.popoverElement.showPopover({ source: this.actionTarget }) } - actionTargetConnected() { - dataset(this.actionTarget, 'action').add( - `click->${this.identifier}#${this.actionTarget.getAttribute('popovertargetaction')}:prevent`, - `keydown.esc@window->${this.identifier}#hide`, - `click@window->${this.identifier}#dismiss`, - ) + toggle() { + !this.open + ? this.show() + : this.hide() + } + + hide() { + this.cleanup?.() + + this.popoverElement.hidePopover() } } diff --git a/components/range/index.js b/components/range/index.js index eb7d43a..44f9895 100644 --- a/components/range/index.js +++ b/components/range/index.js @@ -4,20 +4,13 @@ export class Range extends Controller { static targets = ['start', 'end'] async setValue({ currentTarget, params }) { - const { setValue, setOutputValue, setTrackProperty } = await import('winduum/src/components/range/index.js') + const { setValue, setOutputValue } = await import('winduum/src/components/range/index.js') setValue(currentTarget, { - track: params.track ?? 'start', + track: params?.track ?? 'start', }) setOutputValue(currentTarget, window[currentTarget.getAttribute('aria-labelledby')]) - - setTrackProperty({ - element: this.element.parentElement, - value: currentTarget.value, - min: Number(currentTarget.min) || 0, - max: Number(currentTarget.max) || 100, - }, params.track ?? 'start') } async startTargetConnected() { diff --git a/components/toaster/index.js b/components/toaster/index.js index cd46580..34d57b5 100644 --- a/components/toaster/index.js +++ b/components/toaster/index.js @@ -1,6 +1,17 @@ import { Controller } from '@hotwired/stimulus' export class Toaster extends Controller { + async connect() { + const { toasterObserver } = await import('winduum/src/components/toaster/index.js') + + this.observer = toasterObserver() + this.observer.observe(this.element, { childList: true }) + } + + disconnect() { + this.observer?.disconnect() + } + async close(event) { const { closeToaster } = await import('winduum/src/components/toaster/index.js') diff --git a/package-lock.json b/package-lock.json index f6d74b1..4316349 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "winduum-stimulus", - "version": "2.0.13", + "version": "3.0.0-next.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "winduum-stimulus", - "version": "2.0.13", + "version": "3.0.0-next.0", "dependencies": { "@hotwired/stimulus": "^3.2.2", "@newlogic-digital/utils-js": "^1.2.0", - "winduum": "^2.2.19" + "winduum": "^3.0.0-next.7" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -974,9 +974,9 @@ } }, "node_modules/winduum": { - "version": "2.2.19", - "resolved": "https://registry.npmjs.org/winduum/-/winduum-2.2.19.tgz", - "integrity": "sha512-WDCpKD2V++492PHhUZTK8OOxjBHg5+q6pir1Fg9J8/BbiB1PdGrSLfS3mU+Tmn7ipo9jp9LJp8hJq0/fEhk9vA==", + "version": "3.0.0-next.7", + "resolved": "https://registry.npmjs.org/winduum/-/winduum-3.0.0-next.7.tgz", + "integrity": "sha512-CNuA0yf2XhV9ndSNr8rnkzC0V/tMqmdqInPyMnfvq33jf+C/AUqVS6FaAfum/cvEgnJvV3pud941LG66POasPg==", "engines": { "node": ">=20.0.0", "npm": ">=9.0.0" diff --git a/package.json b/package.json index 68ed22f..860a230 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "winduum-stimulus", - "version": "2.0.15", + "version": "3.0.0-next.0", "type": "module", "scripts": { "eslint": "eslint", @@ -10,7 +10,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@newlogic-digital/utils-js": "^1.2.0", - "winduum": "^2.2.19" + "winduum": "^3.0.0-next.7" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/utilities/invoke/readme.md b/utilities/invoke/readme.md index 70eaa72..1361671 100644 --- a/utilities/invoke/readme.md +++ b/utilities/invoke/readme.md @@ -10,8 +10,8 @@ This way you can place action anywhere you want. To trigger an invoke action, us ```html + +
+

Native popover

+

+ Tohle je obsah popoveru spravovany Stimulus controllerem. +

+ +
+ + + + + diff --git a/package-lock.json b/package-lock.json index 4316349..1342fba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@newlogic-digital/utils-js": "^1.2.0", - "winduum": "^3.0.0-next.7" + "winduum": "^3.0.0-next.11" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -974,9 +974,9 @@ } }, "node_modules/winduum": { - "version": "3.0.0-next.7", - "resolved": "https://registry.npmjs.org/winduum/-/winduum-3.0.0-next.7.tgz", - "integrity": "sha512-CNuA0yf2XhV9ndSNr8rnkzC0V/tMqmdqInPyMnfvq33jf+C/AUqVS6FaAfum/cvEgnJvV3pud941LG66POasPg==", + "version": "3.0.0-next.11", + "resolved": "https://registry.npmjs.org/winduum/-/winduum-3.0.0-next.11.tgz", + "integrity": "sha512-5jFcSU6v5+m4IY586UCcAi05WXT+mXN8OUvJ5AshAEeB/YDCMUQiZ3+wWPy0TbZmf4Rfn/Tops2nCnJf0TfjOg==", "engines": { "node": ">=20.0.0", "npm": ">=9.0.0" diff --git a/package.json b/package.json index 860a230..55bcd91 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@newlogic-digital/utils-js": "^1.2.0", - "winduum": "^3.0.0-next.7" + "winduum": "^3.0.0-next.11" }, "devDependencies": { "@eslint/js": "^10.0.1", From e6f2fa186095994b9887a633406f43fbf1c85741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Bla=C5=BEek?= Date: Wed, 8 Jul 2026 16:48:40 +0200 Subject: [PATCH 05/10] feat(popover): add custom commands and improve popover lifecycle management --- components/popover/index.js | 48 ++++++++++++++++++++++--------------- index.html | 21 +++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/components/popover/index.js b/components/popover/index.js index fd140b2..9baaf1c 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -10,39 +10,49 @@ export class Popover extends Controller { open = false connect() { + this.showPopover = HTMLElement.prototype.showPopover + this.hidePopover = HTMLElement.prototype.hidePopover this.abortController = new AbortController() this.element.addEventListener('toggle', (event) => { this.open = event.newState === 'open' if (this.source?.ariaExpanded) this.source.ariaExpanded = this.open }, { signal: this.abortController.signal }) - } - disconnect() { - this.abortController?.abort() - } + this.element.addEventListener('command', (e) => { + e.preventDefault() + + const method = e.command.replace(/-\w/g, c => c[1].toUpperCase()) + + if (method in this.element) this.element[method](e) + }) + + this.element.showPopover = async ({ source }) => { + if ((this.autoUpdateValue && !supportsAnchoredContainer) || !supportsAnchor) { + const { autoUpdatePopover } = await import('winduum/src/components/popover/index.js') - async show({ currentTarget }) { - if ((this.autoUpdateValue && !supportsAnchoredContainer) || !supportsAnchor) { - const { autoUpdatePopover } = await import('winduum/src/components/popover/index.js') + this.cleanup = await autoUpdatePopover(source, this.element, this.placementValue, this.autoUpdateValue) + } - this.cleanup = await autoUpdatePopover(currentTarget, this.element, this.placementValue, this.autoUpdateValue) + this.source = source + + this.showPopover.call(this.element, { source }) } - this.source = currentTarget + this.element.togglePopover = ({ source }) => { + !this.open + ? this.element.showPopover({ source }) + : this.element.hidePopover() + } - this.element.showPopover({ source: currentTarget }) - } + this.element.hidePopover = () => { + this.cleanup?.() - toggle({ currentTarget }) { - !this.open - ? this.element.showPopover({ source: currentTarget }) - : this.element.hidePopover() + this.hidePopover.call(this.element) + } } - hide() { - this.cleanup?.() - - this.element.hidePopover() + disconnect() { + this.abortController?.abort() } } diff --git a/index.html b/index.html index e1cbd5f..49a2887 100644 --- a/index.html +++ b/index.html @@ -130,20 +130,26 @@

Popover demo

+ + +

Native popover

@@ -153,9 +159,8 @@

Native popover

From 449a91f989ee265cf361426ac1d5ae4ae3089e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Bla=C5=BEek?= Date: Tue, 14 Jul 2026 14:43:14 +0200 Subject: [PATCH 06/10] refactor(toast): inline imports for `closeToast` and `showToast` functions --- components/toast/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/components/toast/index.js b/components/toast/index.js index 8dd126f..c8ae2b1 100644 --- a/components/toast/index.js +++ b/components/toast/index.js @@ -1,4 +1,5 @@ import { Controller } from '@hotwired/stimulus' +import { closeToast, showToast } from 'winduum/src/components/toast' export class Toast extends Controller { static values = { @@ -10,14 +11,10 @@ export class Toast extends Controller { } async show(event) { - const { showToast } = await import('winduum/src/components/toast/index.js') - await showToast(this.element, this.hasParamsValue ? this.paramsValue : event?.params) } async close(event) { - const { closeToast } = await import('winduum/src/components/toast/index.js') - await closeToast(this.element, this.hasParamsValue ? this.paramsValue : event?.params) } } From c0af696887217b8b40addd2e2ca5f778816f653e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Bla=C5=BEek?= Date: Wed, 15 Jul 2026 11:16:54 +0200 Subject: [PATCH 07/10] feat(carousel): add experimental carousel controller with documentation and enhanced features --- components/carousel-experimental/index.js | 60 ++++++++++++++++ components/carousel-experimental/readme.md | 26 +++++++ components/carousel/index.js | 79 ++++++++++++---------- 3 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 components/carousel-experimental/index.js create mode 100644 components/carousel-experimental/readme.md diff --git a/components/carousel-experimental/index.js b/components/carousel-experimental/index.js new file mode 100644 index 0000000..efc451d --- /dev/null +++ b/components/carousel-experimental/index.js @@ -0,0 +1,60 @@ +import { Controller } from '@hotwired/stimulus' +import { dataset } from '@newlogic-digital/utils-js' + +export class Carousel extends Controller { + static targets = ['content', 'markerGroup', 'marker', 'prev', 'next'] + + static values = { + vertical: Boolean, + } + + async connect() { + const { setSnappedAttribute, toggleScrollState } = await import('winduum/src/components/carousel-experimental/index.js') + + this.abortController = new AbortController() + const signal = this.abortController.signal + + this.contentTarget.addEventListener('scrollsnapchanging', (event) => { + setSnappedAttribute(this.contentTarget, event.snapTargetInline ?? event.snapTargetBlock, this.hasMarkerGroupTarget ? this.markerGroupTarget : null) + }, { signal }) + + this.contentTarget.addEventListener('scroll', () => { + toggleScrollState(this.contentTarget, { + prevElement: this.hasPrevTarget ? this.prevTarget : null, + nextElement: this.hasNextTarget ? this.nextTarget : null, + vertical: this.verticalValue, + }) + }, { signal }) + } + + disconnect() { + this.abortController?.abort() + } + + markerTargetConnected(element) { + dataset(element, 'action').add(`click->${this.identifier}#scrollToMarker:prevent`) + } + + async scrollToMarker({ currentTarget }) { + const { scrollToMarker } = await import('winduum/src/components/carousel-experimental/index.js') + + scrollToMarker(this.contentTarget, currentTarget, this.markerGroupTarget, this.verticalValue ? { block: 'start' } : {}) + } + + async scroll(direction) { + const { scrollBy } = await import('winduum/src/components/carousel-experimental/index.js') + + scrollBy(this.contentTarget, { + direction, + vertical: this.verticalValue, + }) + } + + scrollPrev() { + this.scroll(-1) + } + + scrollNext() { + this.scroll(1) + } +} diff --git a/components/carousel-experimental/readme.md b/components/carousel-experimental/readme.md new file mode 100644 index 0000000..30dcfd2 --- /dev/null +++ b/components/carousel-experimental/readme.md @@ -0,0 +1,26 @@ +# [Carousel Experimental](https://winduum.dev/docs/components/carousel-experimental.html) + +## Installation +```shell +npm i winduum-stimulus +``` + +```js +import { Application } from '@hotwired/stimulus' +import { Carousel } from 'winduum-stimulus/components/carousel-experimental/index.js' + +const application = Application.start() + +application.register('x-carousel-experimental', Carousel) +``` + +### Local imports +By default, imports are directly from `npm` so you can leverage updates. +Alternatively, you can also copy and paste the code from this directory to your project and remap the imports to local. + +```js +import { Carousel } from '@/components/ui/carousel-experimental/index.js' +``` + +### Docs +Visit [docs](https://winduum.dev/docs/components/carousel-experimental.html) to learn more about JavaScript API and see usage examples. diff --git a/components/carousel/index.js b/components/carousel/index.js index fa01c6e..fa1b552 100644 --- a/components/carousel/index.js +++ b/components/carousel/index.js @@ -1,60 +1,67 @@ import { Controller } from '@hotwired/stimulus' -import { dataset } from '@newlogic-digital/utils-js' export class Carousel extends Controller { - static targets = ['content', 'markerGroup', 'marker', 'prev', 'next'] + static targets = ['content', 'counterMin', 'counterMax', 'pagination', 'progress', 'prev', 'next'] - static values = { - vertical: Boolean, - } + paginationItemContent = '
' async connect() { - const { setSnappedAttribute, toggleScrollState } = await import('winduum/src/components/carousel/index.js') - - this.abortController = new AbortController() - const signal = this.abortController.signal + await this.scroll() - this.contentTarget.addEventListener('scrollsnapchanging', (event) => { - setSnappedAttribute(this.contentTarget, event.snapTargetInline ?? event.snapTargetBlock, this.hasMarkerGroupTarget ? this.markerGroupTarget : null) - }, { signal }) + if (this.hasPaginationTarget && !this.paginationTarget.children.length) { + const { paginationCarousel } = await import('winduum/src/components/carousel/index.js') - this.contentTarget.addEventListener('scroll', () => { - toggleScrollState(this.contentTarget, { - prevElement: this.hasPrevTarget ? this.prevTarget : null, - nextElement: this.hasNextTarget ? this.nextTarget : null, - vertical: this.verticalValue, + paginationCarousel(this.contentTarget, { + element: this.paginationTarget, + itemContent: this.paginationItemContent, }) - }, { signal }) - } - - disconnect() { - this.abortController?.abort() + } } - markerTargetConnected(element) { - dataset(element, 'action').add(`click->${this.identifier}#scrollToMarker:prevent`) - } + async scroll() { + const { scrollCarousel } = await import('winduum/src/components/carousel/index.js') - async scrollToMarker({ currentTarget }) { - const { scrollToMarker } = await import('winduum/src/components/carousel/index.js') + this.toggleScrollAttributes() - scrollToMarker(this.contentTarget, currentTarget, this.markerGroupTarget, this.verticalValue ? { block: 'start' } : {}) + scrollCarousel(this.contentTarget, { + counterMinElement: this.hasCounterMinTarget && this.counterMinTarget, + counterMaxElement: this.hasCounterMaxTarget && this.counterMaxTarget, + progressElement: this.hasProgressTarget && this.progressTarget, + pagination: { + element: this.hasPaginationTarget && this.paginationTarget, + }, + }) } - async scroll(direction) { - const { scrollBy } = await import('winduum/src/components/carousel/index.js') + toggleScrollAttributes() { + const scrollStart = this.contentTarget.scrollLeft <= 0 + const scrollEnd = this.contentTarget.scrollLeft >= this.contentTarget.scrollWidth - this.contentTarget.clientWidth + const scrollNone = this.contentTarget.scrollWidth - this.contentTarget.clientWidth === 0 - scrollBy(this.contentTarget, { - direction, - vertical: this.verticalValue, - }) + if (this.hasPrevTarget && this.hasNextTarget) { + this.prevTarget.disabled = scrollStart + this.nextTarget.disabled = scrollEnd + } + + this.element.toggleAttribute('data-scroll-start', scrollStart) + this.element.toggleAttribute('data-scroll-end', scrollEnd) + this.element.toggleAttribute('data-scroll-none', scrollNone) } scrollPrev() { - this.scroll(-1) + this.contentTarget.scroll({ left: this.contentTarget.scrollLeft - this.contentTarget.children[0].offsetWidth }) } scrollNext() { - this.scroll(1) + this.contentTarget.scroll({ left: this.contentTarget.scrollLeft + this.contentTarget.children[0].offsetWidth }) + } + + async scrollTo({ currentTarget }) { + const { scrollTo } = await import('winduum/src/components/carousel/index.js') + + const siblingElements = [...currentTarget.parentElement.children] + const index = siblingElements.indexOf(currentTarget) + + scrollTo(this.contentTarget, index) } } From f6f70f58d120a89b5a07c605beb4dab2da2a38c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Bla=C5=BEek?= Date: Wed, 15 Jul 2026 15:51:07 +0200 Subject: [PATCH 08/10] chore(deps): upgrade Winduum to 3.0.0-next.16 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1342fba..3f2ddae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@newlogic-digital/utils-js": "^1.2.0", - "winduum": "^3.0.0-next.11" + "winduum": "^3.0.0-next.16" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -974,9 +974,9 @@ } }, "node_modules/winduum": { - "version": "3.0.0-next.11", - "resolved": "https://registry.npmjs.org/winduum/-/winduum-3.0.0-next.11.tgz", - "integrity": "sha512-5jFcSU6v5+m4IY586UCcAi05WXT+mXN8OUvJ5AshAEeB/YDCMUQiZ3+wWPy0TbZmf4Rfn/Tops2nCnJf0TfjOg==", + "version": "3.0.0-next.16", + "resolved": "https://registry.npmjs.org/winduum/-/winduum-3.0.0-next.16.tgz", + "integrity": "sha512-Nk6GbAATr++Z1gGR46htvbzEaDxikgmmMVXz1DRb6hRJgL3UxqCIlXtKW3zs8yWJ5msvFxWdz1DoR8a/vd8dBg==", "engines": { "node": ">=20.0.0", "npm": ">=9.0.0" diff --git a/package.json b/package.json index 55bcd91..ee4f79c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@newlogic-digital/utils-js": "^1.2.0", - "winduum": "^3.0.0-next.11" + "winduum": "^3.0.0-next.16" }, "devDependencies": { "@eslint/js": "^10.0.1", From 60aa14fc12d124a8ea6e82e83427b0164002a187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Bla=C5=BEek?= Date: Fri, 17 Jul 2026 12:52:55 +0200 Subject: [PATCH 09/10] feat(drawer, popover): integrate Invoker Commands, add `onCommand` helper, and enhance lifecycle cleanup --- CHANGELOG.md | 4 ++- components/drawer/index.js | 50 +++++++++++++++++++------------------ components/drawer/readme.md | 22 ++++++++++++++++ components/popover/index.js | 13 +++++----- index.js | 10 +++++++- package.json | 2 +- utilities/invoke/readme.md | 8 +++--- 7 files changed, 72 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5c321..24add1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # 3.0.0-next.0 (2026-07-02) * feat!: updated components to Winduum `3.0.0-next.7` (aligned with `winduum-elements`) * feat!: carousel rewritten to new API (`scrollBy`, `toggleScrollState`, `setSnappedAttribute`, `scrollToMarker`) — `pagination`, `counter` and `progress` targets replaced by `markerGroup`/`marker` targets and `vertical` value -* feat!: drawer rewritten to new dialog-based API (`drawerEvents`, `drawerObserver`, `showDrawer`) — `dialog` value replaced by boolean `modal` value +* feat!: drawer rewritten to new dialog-based API (`drawerEvents`, `drawerObserver`, `showDrawer`) and native Invoker Commands — `show`, `close` and `toggle` actions removed, `dialog` value replaced by boolean `modal` value * feat!: popover rewritten to native Popover API with anchor positioning and `@floating-ui` fallback (`autoUpdate`, `placement` values) +* feat: added shared `onCommand` helper for command-driven components +* fix: popover now removes its command listener, positioning fallback and overridden element methods on disconnect * feat!: removed dialog and details components (handled natively in Winduum 3) * feat!: form component now only validates on submit — field validation moved to the new field component * feat: added field component diff --git a/components/drawer/index.js b/components/drawer/index.js index 6fabaaa..df59009 100644 --- a/components/drawer/index.js +++ b/components/drawer/index.js @@ -1,4 +1,5 @@ import { Controller } from '@hotwired/stimulus' +import { onCommand } from '../../index.js' export class Drawer extends Controller { static targets = ['content'] @@ -15,14 +16,37 @@ export class Drawer extends Controller { } connect() { + this.showModal = HTMLDialogElement.prototype.showModal + this.show = HTMLDialogElement.prototype.show + this.close = HTMLDialogElement.prototype.close this.abortController = new AbortController() - this.element.addEventListener('close', () => { + this.element.addEventListener('command', onCommand, { signal: this.abortController.signal }) + + this.element.showModal = async ({ source }) => { + const { showDrawer } = await import('winduum/src/components/drawer/index.js') + + this.triggerElement = source + + if (this.element.open) return + + if (this.modalValue) this.showModal.call(this.element) + else this.show.call(this.element) + + source.ariaExpanded = true + showDrawer(this.element.firstElementChild, this.placementValue) + } + + this.element.close = () => { + this.close.call(this.element) + if (this.triggerElement) this.triggerElement.ariaExpanded = false - }, { signal: this.abortController.signal }) + } } disconnect() { + delete this.element.showModal + delete this.element.close this.abortController?.abort() } @@ -38,26 +62,4 @@ export class Drawer extends Controller { contentTargetDisconnected() { this.observer?.disconnect() } - - async show({ currentTarget }) { - const { showDrawer } = await import('winduum/src/components/drawer/index.js') - - this.triggerElement = currentTarget - - if (this.element.open) return - if (this.modalValue) this.element.showModal() - else this.element.show() - - currentTarget.ariaExpanded = true - showDrawer(this.element.firstElementChild, this.placementValue) - } - - close() { - this.element.close() - } - - toggle(event) { - if (this.element.open) this.close() - else this.show(event) - } } diff --git a/components/drawer/readme.md b/components/drawer/readme.md index 1c10fb9..cc22156 100644 --- a/components/drawer/readme.md +++ b/components/drawer/readme.md @@ -14,6 +14,28 @@ const application = Application.start() application.register('x-drawer', Drawer) ``` +Open and close the Drawer with native Invoker Commands. The controller enhances `show-modal` with the Drawer scroll behavior, keeps the invoking button's `aria-expanded` state in sync and lets `request-close` preserve the swipe-style closing animation. + +```html + + + +
+ +
+
+``` + +Use the [`invokers-polyfill`](https://www.npmjs.com/package/invokers-polyfill) when supporting browsers without Invoker Commands. + ### Local imports By default, imports are directly from `npm` so you can leverage updates. Alternatively, you can also copy and paste the code from this directory to your project and remap the imports to local. diff --git a/components/popover/index.js b/components/popover/index.js index 9baaf1c..9653e10 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -1,5 +1,6 @@ import { Controller } from '@hotwired/stimulus' import { supportsAnchoredContainer, supportsAnchor } from 'winduum/src/common.js' +import { onCommand } from '../../index.js' export class Popover extends Controller { static values = { @@ -19,13 +20,7 @@ export class Popover extends Controller { if (this.source?.ariaExpanded) this.source.ariaExpanded = this.open }, { signal: this.abortController.signal }) - this.element.addEventListener('command', (e) => { - e.preventDefault() - - const method = e.command.replace(/-\w/g, c => c[1].toUpperCase()) - - if (method in this.element) this.element[method](e) - }) + this.element.addEventListener('command', onCommand, { signal: this.abortController.signal }) this.element.showPopover = async ({ source }) => { if ((this.autoUpdateValue && !supportsAnchoredContainer) || !supportsAnchor) { @@ -53,6 +48,10 @@ export class Popover extends Controller { } disconnect() { + this.cleanup?.() + delete this.element.showPopover + delete this.element.togglePopover + delete this.element.hidePopover this.abortController?.abort() } } diff --git a/index.js b/index.js index 29ca5f0..e2381a1 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,13 @@ import { dataset } from '@newlogic-digital/utils-js' +export const onCommand = (event) => { + event.preventDefault() + + const method = event.command.replace(/-\w/g, c => c[1].toUpperCase()) + + if (method in event.currentTarget) event.currentTarget[method](event) +} + export function initActions(parent, selectors) { if (!parent) return @@ -35,4 +43,4 @@ export const useController = (controller, target, application) => { return getController } -export default { initStimulus, initActions, initControllers, useController } +export default { onCommand, initStimulus, initActions, initControllers, useController } diff --git a/package.json b/package.json index ee4f79c..bb42c39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "winduum-stimulus", - "version": "3.0.0-next.0", + "version": "3.0.0-next.1", "type": "module", "scripts": { "eslint": "eslint", diff --git a/utilities/invoke/readme.md b/utilities/invoke/readme.md index 1361671..94a5939 100644 --- a/utilities/invoke/readme.md +++ b/utilities/invoke/readme.md @@ -10,13 +10,15 @@ This way you can place action anywhere you want. To trigger an invoke action, us ```html