@ds-mo/ui 15.4.0 → 15.5.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.
- package/NOTICE +7 -1
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +64 -8
- package/dist/angular/boolean-value-accessor.js +3 -3
- package/dist/angular/ds-agent-activity.js +3 -3
- package/dist/angular/ds-agent-questionnaire.js +3 -3
- package/dist/angular/ds-agent-response.js +3 -3
- package/dist/angular/ds-agent-source-list.js +3 -3
- package/dist/angular/ds-agent-tool-call.js +3 -3
- package/dist/angular/ds-attachment-list.js +3 -3
- package/dist/angular/ds-avatar.js +3 -3
- package/dist/angular/ds-badge.js +3 -3
- package/dist/angular/ds-banner.js +3 -3
- package/dist/angular/ds-bar-action.js +3 -3
- package/dist/angular/ds-bar-nav.js +3 -3
- package/dist/angular/ds-bar-page-title.js +3 -3
- package/dist/angular/ds-bar-title.js +3 -3
- package/dist/angular/ds-bar-workflow.js +3 -3
- package/dist/angular/ds-breadcrumb.js +3 -3
- package/dist/angular/ds-button-filled.js +3 -3
- package/dist/angular/ds-button-inverted.js +3 -3
- package/dist/angular/ds-button-unfilled.js +3 -3
- package/dist/angular/ds-calendar.js +3 -3
- package/dist/angular/ds-card-action-center.js +3 -3
- package/dist/angular/ds-card-chart.js +3 -3
- package/dist/angular/ds-card-navigation.js +3 -3
- package/dist/angular/ds-card-overview.js +3 -3
- package/dist/angular/ds-card-setting.js +3 -3
- package/dist/angular/ds-card-settings-scope.js +3 -3
- package/dist/angular/ds-chart-legend.js +3 -3
- package/dist/angular/ds-chart.js +3 -3
- package/dist/angular/ds-checkbox-group.js +3 -3
- package/dist/angular/ds-checkbox.d.ts +1 -1
- package/dist/angular/ds-checkbox.js +5 -5
- package/dist/angular/ds-chip.js +3 -3
- package/dist/angular/ds-code-block.js +3 -3
- package/dist/angular/ds-conversation-list-item.js +3 -3
- package/dist/angular/ds-conversation-list-section.js +3 -3
- package/dist/angular/ds-conversation-list.js +3 -3
- package/dist/angular/ds-data-customize.js +3 -3
- package/dist/angular/ds-data-filter.js +3 -3
- package/dist/angular/ds-data-group.js +3 -3
- package/dist/angular/ds-data-preferences.js +3 -3
- package/dist/angular/ds-data-saved-views.js +3 -3
- package/dist/angular/ds-data-search.js +3 -3
- package/dist/angular/ds-data-sort.js +3 -3
- package/dist/angular/ds-data-toolbar.js +3 -3
- package/dist/angular/ds-divider.js +3 -3
- package/dist/angular/ds-empty-state.js +3 -3
- package/dist/angular/ds-field.js +3 -3
- package/dist/angular/ds-filter-menu.js +3 -3
- package/dist/angular/ds-icon.js +3 -3
- package/dist/angular/ds-inline-banner-settings.js +3 -3
- package/dist/angular/ds-input-date.js +3 -3
- package/dist/angular/ds-input-time.js +3 -3
- package/dist/angular/ds-input.js +3 -3
- package/dist/angular/ds-loader.js +3 -3
- package/dist/angular/ds-map-cluster-marker.js +3 -3
- package/dist/angular/ds-map-entity-marker.js +3 -3
- package/dist/angular/ds-markdown.js +3 -3
- package/dist/angular/ds-menu.js +3 -3
- package/dist/angular/ds-message-actions.js +3 -3
- package/dist/angular/ds-message-bubble.js +3 -3
- package/dist/angular/ds-message-composer.js +3 -3
- package/dist/angular/ds-message-scroller.js +3 -3
- package/dist/angular/ds-message.js +3 -3
- package/dist/angular/ds-mobile-bar-nav.js +3 -3
- package/dist/angular/ds-mobile-header.js +3 -3
- package/dist/angular/ds-mobile-section-switcher.js +3 -3
- package/dist/angular/ds-mobile-sheet-nav.js +3 -3
- package/dist/angular/ds-modal.js +3 -3
- package/dist/angular/ds-pagination.js +3 -3
- package/dist/angular/ds-panel-nav.js +3 -3
- package/dist/angular/ds-panel-sub-nav.js +3 -3
- package/dist/angular/ds-panel-tool-header.js +3 -3
- package/dist/angular/ds-panel-tool-search.js +3 -3
- package/dist/angular/ds-panel-tools.js +3 -3
- package/dist/angular/ds-paper-texture.js +3 -3
- package/dist/angular/ds-radio-tile.js +3 -3
- package/dist/angular/ds-radio.js +3 -3
- package/dist/angular/ds-score.js +3 -3
- package/dist/angular/ds-scroll-overlay.js +3 -3
- package/dist/angular/ds-select.d.ts +1 -1
- package/dist/angular/ds-select.js +5 -5
- package/dist/angular/ds-setting-row-checkbox.js +3 -3
- package/dist/angular/ds-setting-row-radio.js +3 -3
- package/dist/angular/ds-setting-row-toggle.js +3 -3
- package/dist/angular/ds-shell-app.js +3 -3
- package/dist/angular/ds-shell-page.js +3 -3
- package/dist/angular/ds-shell-tools.js +3 -3
- package/dist/angular/ds-skeleton.js +3 -3
- package/dist/angular/ds-slider.js +3 -3
- package/dist/angular/ds-swatch-picker.js +3 -3
- package/dist/angular/ds-switch.js +3 -3
- package/dist/angular/ds-tab-group.js +3 -3
- package/dist/angular/ds-table.d.ts +7 -1
- package/dist/angular/ds-table.js +13 -7
- package/dist/angular/ds-tag.js +3 -3
- package/dist/angular/ds-text.js +3 -3
- package/dist/angular/ds-textarea.js +3 -3
- package/dist/angular/ds-time-picker.js +3 -3
- package/dist/angular/ds-toast.js +3 -3
- package/dist/angular/ds-tooltip-chart.js +3 -3
- package/dist/angular/ds-tooltip.js +3 -3
- package/dist/angular/ds-typing-indicator.js +3 -3
- package/dist/angular/number-value-accessor.js +3 -3
- package/dist/angular/radio-value-accessor.js +3 -3
- package/dist/angular/select-value-accessor.js +3 -3
- package/dist/angular/text-value-accessor.js +3 -3
- package/dist/angular/value-accessor.js +3 -3
- package/dist/components/THIRD-PARTY-NOTICES +1791 -0
- package/dist/components/bundled-dependencies.json +478 -0
- package/dist/components/ds-agent-activity.js +1 -1
- package/dist/components/ds-agent-questionnaire.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-agent-source-list.js +1 -1
- package/dist/components/ds-agent-tool-call.js +1 -1
- package/dist/components/ds-attachment-list.js +1 -1
- package/dist/components/ds-avatar.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-banner.js.map +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-page-title.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-breadcrumb.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-inverted.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-calendar.js +1 -1
- package/dist/components/ds-card-action-center.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-navigation.js +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-overview.js.map +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart-legend.js.map +1 -1
- package/dist/components/ds-chart.js +1 -1
- package/dist/components/ds-chart.js.map +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list.js +1 -1
- package/dist/components/ds-conversation-list.js.map +1 -1
- package/dist/components/ds-data-customize.js +1 -1
- package/dist/components/ds-data-filter.js +1 -1
- package/dist/components/ds-data-group.js +1 -1
- package/dist/components/ds-data-preferences.js +1 -1
- package/dist/components/ds-data-preferences.js.map +1 -1
- package/dist/components/ds-data-saved-views.js +1 -1
- package/dist/components/ds-data-search.js +1 -1
- package/dist/components/ds-data-sort.js +1 -1
- package/dist/components/ds-data-toolbar.js +1 -1
- package/dist/components/ds-divider.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input-date.js +1 -1
- package/dist/components/ds-input-date.js.map +1 -1
- package/dist/components/ds-input-time.js +1 -1
- package/dist/components/ds-input-time.js.map +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-map-entity-marker.js +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-bubble.js +1 -1
- package/dist/components/ds-message-composer.js +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-message.js +1 -1
- package/dist/components/ds-mobile-bar-nav.js +1 -1
- package/dist/components/ds-mobile-header.js +1 -1
- package/dist/components/ds-mobile-section-switcher.js +1 -1
- package/dist/components/ds-mobile-sheet-nav.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-pagination.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js +1 -1
- package/dist/components/ds-panel-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-radio-tile.js +1 -1
- package/dist/components/ds-radio-tile.js.map +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-scroll-overlay.js +1 -1
- package/dist/components/ds-scroll-overlay.js.map +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-setting-row-toggle.js +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-switch.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-textarea.js +1 -1
- package/dist/components/ds-textarea.js.map +1 -1
- package/dist/components/ds-time-picker.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-tooltip-chart.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/p--STJbKuN.js +2 -0
- package/dist/components/p--STJbKuN.js.map +1 -0
- package/dist/components/{p-Co1rUdOR.js → p--pJyciTI.js} +2 -2
- package/dist/components/p--pJyciTI.js.map +1 -0
- package/dist/components/p-02k7d7BH.js +2 -0
- package/dist/components/p-02k7d7BH.js.map +1 -0
- package/dist/components/{p-CZNZTcYS.js → p-5-gQbIIz.js} +2 -2
- package/dist/components/{p-DRdoGSGX.js → p-7MwoWtTJ.js} +2 -2
- package/dist/components/p-B0uO3-Dp.js +2 -0
- package/dist/components/p-B0uO3-Dp.js.map +1 -0
- package/dist/components/p-B6TX55c5.js +2 -0
- package/dist/components/p-B6TX55c5.js.map +1 -0
- package/dist/components/p-B95UP66y.js +2 -0
- package/dist/components/p-B95UP66y.js.map +1 -0
- package/dist/components/p-BGSG6xIS.js +2 -0
- package/dist/components/p-BGSG6xIS.js.map +1 -0
- package/dist/components/p-BNarCFJZ.js +2 -0
- package/dist/components/p-BNarCFJZ.js.map +1 -0
- package/dist/components/{p-DJDbskZf.js → p-BO5URdZF.js} +2 -2
- package/dist/components/{p-DmSnHVW6.js → p-BVxK8ce0.js} +2 -2
- package/dist/components/{p-B0EpvBDs.js → p-BYeG0b5N.js} +2 -2
- package/dist/components/p-BcpIK9S6.js +2 -0
- package/dist/components/p-BcpIK9S6.js.map +1 -0
- package/dist/components/p-Bf-BphXP.js +2 -0
- package/dist/components/p-Bf-BphXP.js.map +1 -0
- package/dist/components/{p-Bk3poU91.js → p-BmIllmq2.js} +2 -2
- package/dist/components/p-BmIllmq2.js.map +1 -0
- package/dist/components/p-Bq_ypMSy.js +2 -0
- package/dist/components/p-Bq_ypMSy.js.map +1 -0
- package/dist/components/p-ByLH5RAD.js +2 -0
- package/dist/components/p-ByLH5RAD.js.map +1 -0
- package/dist/components/{p-DzvTC27M.js → p-C1ZfBbJ_.js} +2 -2
- package/dist/components/p-C1ZfBbJ_.js.map +1 -0
- package/dist/components/p-C1oczmAc.js +2 -0
- package/dist/components/p-C1oczmAc.js.map +1 -0
- package/dist/components/p-C5BBIDVm.js +2 -0
- package/dist/components/p-C5BBIDVm.js.map +1 -0
- package/dist/components/{p-CJ1NnOEW.js → p-CA3iQ1ME.js} +2 -2
- package/dist/components/p-CE6MaXBg.js +2 -0
- package/dist/components/p-CE6MaXBg.js.map +1 -0
- package/dist/components/p-CXGjCWSh.js +2 -0
- package/dist/components/p-CXGjCWSh.js.map +1 -0
- package/dist/components/{p-DYQa1DVF.js → p-C_9VGgPf.js} +2 -2
- package/dist/components/p-CbrDF2gA.js +2 -0
- package/dist/components/p-CbrDF2gA.js.map +1 -0
- package/dist/components/{p-7TVdPhGR.js → p-CgQB8Ct5.js} +2 -2
- package/dist/components/{p-D6FRqNhN.js → p-D-J7GgjY.js} +2 -2
- package/dist/components/{p-DSZPO06T.js → p-D9_iDlkT.js} +2 -2
- package/dist/components/{p-BwdnVqAY.js → p-DEYCF-oJ.js} +2 -2
- package/dist/components/p-DGVZhrMd.js +2 -0
- package/dist/components/p-DGVZhrMd.js.map +1 -0
- package/dist/components/{p-C8V4lkVy.js → p-DKGW3gRa.js} +2 -2
- package/dist/components/p-DPFg5d4_.js +2 -0
- package/dist/components/p-DPFg5d4_.js.map +1 -0
- package/dist/components/{p-C4huuwed.js → p-DR_8IPn9.js} +2 -2
- package/dist/components/p-DXL5D30P.js +2 -0
- package/dist/components/p-DXL5D30P.js.map +1 -0
- package/dist/components/{p-B0BQCVkq.js → p-D_iZPukF.js} +2 -2
- package/dist/components/{p-JBki_jyY.js → p-DfMTDFcE.js} +2 -2
- package/dist/components/{p-MOJmB6vX.js → p-Dg6hnCc2.js} +2 -2
- package/dist/components/{p-DXk1eml1.js → p-DjbT9kKm.js} +2 -2
- package/dist/components/p-Dka0lUI_.js +2 -0
- package/dist/components/p-Dka0lUI_.js.map +1 -0
- package/dist/components/p-DlsjHRU3.js +2 -0
- package/dist/components/p-DlsjHRU3.js.map +1 -0
- package/dist/components/{p-_IqSPt5V.js → p-Dr5rkAhe.js} +2 -2
- package/dist/components/p-DuCCuJKI.js +2 -0
- package/dist/components/p-DuCCuJKI.js.map +1 -0
- package/dist/components/{p-DIQiHX0q.js → p-DvGrsLgt.js} +2 -2
- package/dist/components/{p-Doa2blZT.js → p-PtxCwf5S.js} +2 -2
- package/dist/components/p-PzYJqN8B.js +2 -0
- package/dist/components/p-PzYJqN8B.js.map +1 -0
- package/dist/components/p-S1Ct3FJV.js +2 -0
- package/dist/components/p-S1Ct3FJV.js.map +1 -0
- package/dist/components/p-YC7UM4jH.js +2 -0
- package/dist/components/p-YC7UM4jH.js.map +1 -0
- package/dist/components/{p-B9TM6FqC.js → p-d76Ry5nh.js} +2 -2
- package/dist/components/p-ioGH5SoA.js +2 -0
- package/dist/components/p-ioGH5SoA.js.map +1 -0
- package/dist/components/p-jp7hwRtC.js +2 -0
- package/dist/components/p-jp7hwRtC.js.map +1 -0
- package/dist/components/{p-BFHpU-uU.js → p-kwS-vkau.js} +2 -2
- package/dist/components/{p-DjkUFlDW.js → p-pPtjNRqC.js} +3 -2
- package/dist/components/p-pPtjNRqC.js.map +1 -0
- package/dist/components/{p-B39iJO8F.js → p-rmPIcWED.js} +2 -2
- package/dist/components/p-t2Ebn8si.js +2 -0
- package/dist/components/p-t2Ebn8si.js.map +1 -0
- package/dist/components/p-wRhYxngW.js +2 -0
- package/dist/components/p-wRhYxngW.js.map +1 -0
- package/dist/docs/components.json +477 -82
- package/dist/lib/components/Icon/icon-cache.d.ts +6 -0
- package/dist/lib/utils/clock-time.d.ts +1 -0
- package/dist/lib/utils/date-filter-value.d.ts +2 -0
- package/dist/lib/utils/form-association.d.ts +2 -1
- package/dist/lib/utils/format-percentage.d.ts +1 -1
- package/dist/lib/utils/index.js +113 -93
- package/dist/lib/utils/index.js.map +4 -4
- package/dist/lib/utils/intl-formatters.d.ts +3 -0
- package/dist/lib/utils/resolve-css-length-px.d.ts +4 -3
- package/dist/lint/THIRD-PARTY-NOTICES +97 -40
- package/dist/lint/bundled-dependencies.json +162 -0
- package/dist/lint/index.js +1341 -1333
- package/dist/mcp/THIRD-PARTY-NOTICES +470 -0
- package/dist/mcp/bundled-dependencies.json +29 -0
- package/dist/mcp-data/registry/agent-tool-call.json +1 -1
- package/dist/mcp-data/registry/banner.json +1 -1
- package/dist/mcp-data/registry/bar-nav.json +1 -1
- package/dist/mcp-data/registry/bar-page-title.json +1 -1
- package/dist/mcp-data/registry/breadcrumb.json +1 -1
- package/dist/mcp-data/registry/calendar.json +4 -4
- package/dist/mcp-data/registry/card-overview.json +1 -1
- package/dist/mcp-data/registry/chart.json +3 -3
- package/dist/mcp-data/registry/checkbox.json +17 -1
- package/dist/mcp-data/registry/conversation-list.json +1 -1
- package/dist/mcp-data/registry/data-group.json +1 -1
- package/dist/mcp-data/registry/data-preferences.json +1 -1
- package/dist/mcp-data/registry/filter-menu.json +1 -1
- package/dist/mcp-data/registry/icon.json +6 -5
- package/dist/mcp-data/registry/input-date.json +1 -1
- package/dist/mcp-data/registry/input-time.json +1 -1
- package/dist/mcp-data/registry/markdown.json +1 -1
- package/dist/mcp-data/registry/menu.json +1 -1
- package/dist/mcp-data/registry/message.json +4 -4
- package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
- package/dist/mcp-data/registry/modal.json +1 -1
- package/dist/mcp-data/registry/panel-nav.json +1 -1
- package/dist/mcp-data/registry/radio-tile.json +1 -1
- package/dist/mcp-data/registry/radio.json +1 -1
- package/dist/mcp-data/registry/registry.json +63 -7
- package/dist/mcp-data/registry/scroll-overlay.json +1 -1
- package/dist/mcp-data/registry/select.json +70 -3
- package/dist/mcp-data/registry/shell-app.json +1 -1
- package/dist/mcp-data/registry/slider.json +3 -3
- package/dist/mcp-data/registry/switch.json +1 -1
- package/dist/mcp-data/registry/table.json +140 -14
- package/dist/mcp-data/registry/tooltip-chart.json +1 -1
- package/dist/react/ds-table.d.ts +3 -1
- package/dist/react/ds-table.js +2 -0
- package/dist/styles/table.css +405 -8
- package/dist/types/components/AgentToolCall/AgentToolCall.d.ts +3 -0
- package/dist/types/components/Banner/Banner.d.ts +1 -0
- package/dist/types/components/BarNav/BarNav.d.ts +4 -0
- package/dist/types/components/BarPageTitle/BarPageTitle.d.ts +4 -0
- package/dist/types/components/Breadcrumb/Breadcrumb.d.ts +5 -0
- package/dist/types/components/Calendar/Calendar.d.ts +2 -0
- package/dist/types/components/CardOverview/CardOverview.d.ts +3 -0
- package/dist/types/components/Chart/Chart.d.ts +7 -1
- package/dist/types/components/Checkbox/Checkbox.d.ts +3 -0
- package/dist/types/components/ConversationList/ConversationList.d.ts +3 -0
- package/dist/types/components/DataGroup/DataGroup.d.ts +3 -0
- package/dist/types/components/DataPreferences/DataPreferences.d.ts +1 -0
- package/dist/types/components/FilterMenu/FilterMenu.d.ts +1 -0
- package/dist/types/components/Icon/icon-cache.d.ts +6 -0
- package/dist/types/components/Icon/icon-svg.d.ts +10 -24
- package/dist/types/components/Markdown/Markdown.d.ts +4 -0
- package/dist/types/components/Menu/Menu.d.ts +3 -0
- package/dist/types/components/Message/Message.d.ts +1 -1
- package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +5 -0
- package/dist/types/components/Modal/Modal.d.ts +3 -0
- package/dist/types/components/PanelNav/PanelNav.d.ts +8 -1
- package/dist/types/components/Radio/Radio.d.ts +1 -0
- package/dist/types/components/ScrollOverlay/ScrollOverlay.d.ts +1 -0
- package/dist/types/components/Select/Select.d.ts +8 -0
- package/dist/types/components/ShellApp/ShellApp.d.ts +3 -0
- package/dist/types/components/Slider/Slider.d.ts +1 -1
- package/dist/types/components/Switch/Switch.d.ts +2 -0
- package/dist/types/components/Table/Table.d.ts +35 -0
- package/dist/types/components/Table/index.d.ts +1 -1
- package/dist/types/components/Table/table-body-renderer.d.ts +1 -0
- package/dist/types/components/Table/table-cell-editor.d.ts +4 -0
- package/dist/types/components/Table/table-grid-model.d.ts +23 -0
- package/dist/types/components/Table/table-layout-controller.d.ts +1 -0
- package/dist/types/components/Table/table-model.d.ts +1 -1
- package/dist/types/components/Table/table-review-fixtures.d.ts +10 -0
- package/dist/types/components/Table/table-row-view.d.ts +11 -2
- package/dist/types/components/Table/table-skeleton-view.d.ts +3 -1
- package/dist/types/components/Table/table-structure.d.ts +38 -0
- package/dist/types/components/Table/table-types.d.ts +71 -1
- package/dist/types/components/Table/table-virtual-controller.d.ts +1 -0
- package/dist/types/components/TooltipChart/TooltipChart.d.ts +4 -0
- package/dist/types/components.d.ts +98 -12
- package/dist/types/shell/chrome-transition.d.ts +1 -0
- package/dist/types/utils/anchored-position-controller.d.ts +2 -0
- package/dist/types/utils/clock-time.d.ts +1 -0
- package/dist/types/utils/connection-tasks.d.ts +13 -0
- package/dist/types/utils/date-filter-value.d.ts +2 -0
- package/dist/types/utils/form-association.d.ts +2 -1
- package/dist/types/utils/format-percentage.d.ts +1 -1
- package/dist/types/utils/intl-formatters.d.ts +4 -0
- package/dist/types/utils/resolve-css-length-px.d.ts +4 -3
- package/dist/types/utils/select-controller.d.ts +1 -0
- package/dist/vue/THIRD-PARTY-NOTICES +14 -0
- package/dist/vue/bundled-dependencies.json +13 -0
- package/dist/vue/ds-checkbox.js +1 -0
- package/dist/vue/ds-select.js +4 -0
- package/dist/vue/ds-table.js +8 -0
- package/package.json +16 -11
- package/dist/components/p-B8ZonS3S.js +0 -2
- package/dist/components/p-B8ZonS3S.js.map +0 -1
- package/dist/components/p-BDdlJZyL.js +0 -2
- package/dist/components/p-BDdlJZyL.js.map +0 -1
- package/dist/components/p-BERXvOCf.js +0 -2
- package/dist/components/p-BERXvOCf.js.map +0 -1
- package/dist/components/p-BQspV5U7.js +0 -2
- package/dist/components/p-BQspV5U7.js.map +0 -1
- package/dist/components/p-BhjqYTiY.js +0 -2
- package/dist/components/p-BhjqYTiY.js.map +0 -1
- package/dist/components/p-BjtXYFhw.js +0 -2
- package/dist/components/p-BjtXYFhw.js.map +0 -1
- package/dist/components/p-Bk3poU91.js.map +0 -1
- package/dist/components/p-BxduVc5g.js +0 -2
- package/dist/components/p-BxduVc5g.js.map +0 -1
- package/dist/components/p-C0qN3PSb.js +0 -2
- package/dist/components/p-C0qN3PSb.js.map +0 -1
- package/dist/components/p-CPamUzaj.js +0 -2
- package/dist/components/p-CPamUzaj.js.map +0 -1
- package/dist/components/p-CUDocY23.js +0 -2
- package/dist/components/p-CUDocY23.js.map +0 -1
- package/dist/components/p-CX6ZxUQT.js +0 -2
- package/dist/components/p-CX6ZxUQT.js.map +0 -1
- package/dist/components/p-Cgn-BORA.js +0 -2
- package/dist/components/p-Cgn-BORA.js.map +0 -1
- package/dist/components/p-Ck1hgD6g.js +0 -2
- package/dist/components/p-Ck1hgD6g.js.map +0 -1
- package/dist/components/p-Co1rUdOR.js.map +0 -1
- package/dist/components/p-CochmPyg.js +0 -2
- package/dist/components/p-CochmPyg.js.map +0 -1
- package/dist/components/p-DCyJ3_PL.js +0 -2
- package/dist/components/p-DCyJ3_PL.js.map +0 -1
- package/dist/components/p-DVEx-Xg9.js +0 -2
- package/dist/components/p-DVEx-Xg9.js.map +0 -1
- package/dist/components/p-DYsE6HLk.js +0 -2
- package/dist/components/p-DYsE6HLk.js.map +0 -1
- package/dist/components/p-DebLYW7A.js +0 -2
- package/dist/components/p-DebLYW7A.js.map +0 -1
- package/dist/components/p-DjkUFlDW.js.map +0 -1
- package/dist/components/p-Dnt5xUBD.js +0 -2
- package/dist/components/p-Dnt5xUBD.js.map +0 -1
- package/dist/components/p-Dyqd-cub.js +0 -2
- package/dist/components/p-Dyqd-cub.js.map +0 -1
- package/dist/components/p-DzvTC27M.js.map +0 -1
- package/dist/components/p-g0jsfEXz.js +0 -2
- package/dist/components/p-g0jsfEXz.js.map +0 -1
- package/dist/components/p-pp1IQN4Z.js +0 -2
- package/dist/components/p-pp1IQN4Z.js.map +0 -1
- package/dist/components/p-rCeztgVn.js +0 -2
- package/dist/components/p-rCeztgVn.js.map +0 -1
- /package/dist/components/{p-CZNZTcYS.js.map → p-5-gQbIIz.js.map} +0 -0
- /package/dist/components/{p-DRdoGSGX.js.map → p-7MwoWtTJ.js.map} +0 -0
- /package/dist/components/{p-DJDbskZf.js.map → p-BO5URdZF.js.map} +0 -0
- /package/dist/components/{p-DmSnHVW6.js.map → p-BVxK8ce0.js.map} +0 -0
- /package/dist/components/{p-B0EpvBDs.js.map → p-BYeG0b5N.js.map} +0 -0
- /package/dist/components/{p-CJ1NnOEW.js.map → p-CA3iQ1ME.js.map} +0 -0
- /package/dist/components/{p-DYQa1DVF.js.map → p-C_9VGgPf.js.map} +0 -0
- /package/dist/components/{p-7TVdPhGR.js.map → p-CgQB8Ct5.js.map} +0 -0
- /package/dist/components/{p-D6FRqNhN.js.map → p-D-J7GgjY.js.map} +0 -0
- /package/dist/components/{p-DSZPO06T.js.map → p-D9_iDlkT.js.map} +0 -0
- /package/dist/components/{p-BwdnVqAY.js.map → p-DEYCF-oJ.js.map} +0 -0
- /package/dist/components/{p-C8V4lkVy.js.map → p-DKGW3gRa.js.map} +0 -0
- /package/dist/components/{p-C4huuwed.js.map → p-DR_8IPn9.js.map} +0 -0
- /package/dist/components/{p-B0BQCVkq.js.map → p-D_iZPukF.js.map} +0 -0
- /package/dist/components/{p-JBki_jyY.js.map → p-DfMTDFcE.js.map} +0 -0
- /package/dist/components/{p-MOJmB6vX.js.map → p-Dg6hnCc2.js.map} +0 -0
- /package/dist/components/{p-DXk1eml1.js.map → p-DjbT9kKm.js.map} +0 -0
- /package/dist/components/{p-_IqSPt5V.js.map → p-Dr5rkAhe.js.map} +0 -0
- /package/dist/components/{p-DIQiHX0q.js.map → p-DvGrsLgt.js.map} +0 -0
- /package/dist/components/{p-Doa2blZT.js.map → p-PtxCwf5S.js.map} +0 -0
- /package/dist/components/{p-B9TM6FqC.js.map → p-d76Ry5nh.js.map} +0 -0
- /package/dist/components/{p-BFHpU-uU.js.map → p-kwS-vkau.js.map} +0 -0
- /package/dist/components/{p-B39iJO8F.js.map → p-rmPIcWED.js.map} +0 -0
|
@@ -931,7 +931,7 @@
|
|
|
931
931
|
},
|
|
932
932
|
{
|
|
933
933
|
"path": "src/wc/components/FilterMenu/FilterMenu.tsx",
|
|
934
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n type VNode,\n Watch,\n} from '@stencil/core';\nimport {\n CONTROL_TEXT_VARIANT,\n DATE_FILTER_RELATIVE_PRESETS,\n controlWidthClass,\n dateFilterRelativeValue,\n parseDateFilterValue,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ControlWidth,\n} from '../../utils';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport type { AnchoredAlign, AnchoredSide } from '../../utils/anchored-position';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { ChoiceOptionRow, ChoiceSearch } from '../../utils/choice-list-parts';\nimport { choiceListUsesSubtext } from '../../utils/choice-list';\n\nexport type FilterMenuFilterKind = 'single' | 'multiple' | 'boolean' | 'date';\nexport type FilterMenuSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type FilterMenuWidth = ControlWidth;\nexport type FilterMenuFooterLayout = 'summary' | 'categories-clear';\nexport type FilterMenuMatchMode = 'any' | 'all';\ntype FilterMenuDateMode = 'range' | 'relative';\n\nexport interface FilterMenuOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport interface FilterMenuFilter {\n id: string;\n label: string;\n kind: FilterMenuFilterKind;\n options?: FilterMenuOption[];\n description?: string;\n fieldLabel?: string;\n /** Draw a standard menu-section divider after this category. */\n divider?: boolean;\n}\n\nexport type FilterMenuValue = string[] | boolean | string;\nexport type FilterMenuValues = Record<string, FilterMenuValue | undefined>;\nexport type FilterMenuMatchModes = Record<string, FilterMenuMatchMode | undefined>;\n\nexport interface FilterMenuChangeDetail {\n filterId: string;\n value: FilterMenuValue;\n}\n\nexport interface FilterMenuMatchModeChangeDetail {\n filterId: string;\n mode: FilterMenuMatchMode;\n}\n\nlet filterMenuId = 0;\n\nconst ICON_SIZE: Record<FilterMenuSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst DATE_MODE_TABS = [\n { id: 'relative', label: 'Relative' },\n { id: 'range', label: 'Range' },\n];\n\nconst RELATIVE_DATE_OPTIONS: FilterMenuOption[] = DATE_FILTER_RELATIVE_PRESETS.map(preset => ({\n label: preset.label,\n value: dateFilterRelativeValue(preset.value),\n}));\n\n@Component({\n tag: 'ds-filter-menu',\n styleUrl: 'FilterMenu.css',\n scoped: true,\n})\nexport class FilterMenu {\n @Element() el!: HTMLElement;\n\n /** Controlled popup visibility. */\n /** Render content inside a parent-owned surface; the parent owns dismissal and positioning. */\n @Prop() embedded: boolean = false;\n /** Stacked, collapsible sections for a configuration panel. */\n @Prop() vertical: boolean = false;\n @Prop({ mutable: true }) open: boolean = false;\n /** Select trigger text. */\n @Prop() triggerLabel: string = 'Filters';\n /** Append the active-filter count to the visible trigger label. */\n @Prop() showSelectedCount: boolean = true;\n /** Optional select trigger prefix icon name. */\n @Prop() icon: string | undefined = 'Filters';\n /** Select trigger density. */\n @Prop() size: FilterMenuSize = 'md';\n /** Select trigger width fit. */\n @Prop() width: FilterMenuWidth = 'hug';\n /** Show the surface-aware inset border around the select trigger. */\n /** Show the trigger chevron. */\n @Prop() showIndicator: boolean = true;\n @Prop() hasBorder: boolean = true;\n /** Show selected interaction fill when one or more criteria are active. */\n /** Keep trigger colors neutral when a value is selected. */\n @Prop() neutralTrigger: boolean = false;\n /** Stage filter edits until Apply is pressed. */\n @Prop() applyRequired: boolean = false;\n @State() private draftValues?: FilterMenuValues;\n @State() private draftModes?: FilterMenuMatchModes;\n\n private changeValue(detail: FilterMenuChangeDetail): void {\n if (this.applyRequired)\n this.draftValues = { ...(this.draftValues ?? this.values), [detail.filterId]: detail.value };\n else this.dsChange.emit(detail);\n }\n private changeMode(detail: FilterMenuMatchModeChangeDetail): void {\n if (this.applyRequired)\n this.draftModes = { ...(this.draftModes ?? this.matchModes), [detail.filterId]: detail.mode };\n else this.dsMatchModeChange.emit(detail);\n }\n private get hasPendingChanges(): boolean {\n const normalize = (value: FilterMenuValue | undefined) =>\n Array.isArray(value) ? (value.length ? [...value].sort() : undefined) : value || undefined;\n const values = this.draftValues ?? this.values;\n const modes = this.draftModes ?? this.matchModes;\n return this.filters.some(\n filter =>\n JSON.stringify(normalize(values[filter.id])) !==\n JSON.stringify(normalize(this.values[filter.id])) ||\n (modes[filter.id] ?? 'any') !== (this.matchModes[filter.id] ?? 'any')\n );\n }\n private applyDraft(): void {\n if (!this.hasPendingChanges) return;\n if (this.draftValues && Object.keys(this.draftValues).length === 0) {\n this.dsClear.emit();\n } else if (this.draftValues) {\n for (const id of new Set([...Object.keys(this.values), ...Object.keys(this.draftValues)])) {\n if (JSON.stringify(this.values[id]) !== JSON.stringify(this.draftValues[id]))\n this.dsChange.emit({\n filterId: id,\n value:\n this.draftValues[id] ??\n (Array.isArray(this.values[id])\n ? []\n : typeof this.values[id] === 'boolean'\n ? false\n : ''),\n });\n }\n }\n if (this.draftModes) {\n for (const filterId of new Set([\n ...Object.keys(this.matchModes),\n ...Object.keys(this.draftModes),\n ])) {\n const mode = this.draftModes[filterId] ?? 'any';\n if ((this.matchModes[filterId] ?? 'any') !== mode)\n this.dsMatchModeChange.emit({ filterId, mode });\n }\n }\n }\n @Prop() activeFill: boolean = false;\n /**\n * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n * visible label and chevron; keep an accessible name via aria-label.\n */\n @Prop({ reflect: true }) collapseLabel: boolean = false;\n /** ID applied to the internal select trigger. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name for the internal select trigger. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Product-owned filter categories and option definitions. */\n @Prop() filters: FilterMenuFilter[] = [];\n /** Controlled values keyed by filter id. */\n @Prop() values: FilterMenuValues = {};\n /** Controlled any/all match mode keyed by multiple-choice filter id. Defaults to any. */\n @Prop() matchModes: FilterMenuMatchModes = {};\n /** Controlled category shown in the option pane. */\n @Prop() activeFilterId: string | undefined;\n /** External trigger element to position against. */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element used for positioning and focus return. */\n @Prop() anchorId: string | undefined;\n /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | undefined;\n /** Preferred popup side; collision handling may flip it. */\n @Prop() side: AnchoredSide = 'bottom';\n /** Cross-axis alignment against the trigger. */\n @Prop() align: AnchoredAlign = 'end';\n /** Gap between trigger and popup — number in px or a TokoMo length. */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number in px or a TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n /** Popup width. It remains clamped to its collision boundary by the component recipe. */\n @Prop() menuWidth: string = TOKEN_CSS_LENGTHS.menuWidthLg;\n /** Accessible name for the non-modal filter dialog. */\n @Prop() menuLabel: string = 'Filters';\n /** Accessible name for the category tab list. */\n @Prop() categoriesLabel: string = 'Filter categories';\n /** Footer action and date-clear accessible label. */\n @Prop() clearLabel: string = 'Clear';\n /** Placeholder shown in each non-date option search header. */\n @Prop() searchPlaceholder: string = 'Search';\n /** Empty-state text shown when an option search has no matches. */\n @Prop() noResultsText: string = 'No results';\n /** Footer recipe: full-width selected summary or reserved category-pane Clear action. */\n @Prop() footerLayout: FilterMenuFooterLayout = 'summary';\n /** Show a visible focus ring on initial entry after keyboard activation. */\n @Prop() initialFocusVisible: boolean = false;\n\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusRingVisible = false;\n @State() private activeOptionIndex = 0;\n @State() private captionCompact = false;\n @State() private dateModeByFilter: Record<string, FilterMenuDateMode> = {};\n @State() private optionQueryByFilter: Record<string, string> = {};\n\n /** Requests a controlled value replacement without closing the popup. */\n @Event() dsChange!: EventEmitter<FilterMenuChangeDetail>;\n /** Requests that the consumer clear every filter value. */\n @Event() dsClear!: EventEmitter<void>;\n /** Requests a controlled any/all mode replacement for a multiple-choice filter. */\n @Event() dsMatchModeChange!: EventEmitter<FilterMenuMatchModeChangeDetail>;\n /** Requests a controlled active-category replacement. */\n @Event() dsActiveFilterChange!: EventEmitter<string>;\n /** Requests that the controlled popup close. */\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted whenever internal select-trigger activation changes popup visibility. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted after exit motion and rendered popup removal complete. */\n @Event() dsAfterClose!: EventEmitter<void>;\n\n private readonly generatedId = `ds-filter-menu-${++filterMenuId}`;\n private triggerElement: HTMLButtonElement | null = null;\n private pendingInitialFocusVisible = false;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n private closingSnapshot: {\n filters: FilterMenuFilter[];\n values: FilterMenuValues;\n matchModes: FilterMenuMatchModes;\n activeFilterId: string | undefined;\n } | null = null;\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const anchorRect = anchor.getBoundingClientRect();\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: this.side,\n align: this.align,\n sideOffsetPx: resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: resolveCssLengthPx(this.alignOffset, 0),\n sectionInsetPx,\n }),\n viewportPadPx: this.viewportPad,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor, this.boundary),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.disconnectCaptionCompactObserver();\n this.position.unobserve();\n this.teardownListeners();\n }\n\n @Watch('collapseLabel')\n onCollapseLabelChange() {\n this.syncCaptionCompactObserver();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (!isOpen) {\n this.draftValues = undefined;\n this.draftModes = undefined;\n }\n if (this.embedded) return;\n if (isOpen) {\n this.teardownListeners();\n this.closingSnapshot = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.pendingInitialFocusVisible || this.initialFocusVisible;\n this.setupListeners();\n this.position.schedule(() => this.focusInitialCategory());\n return;\n }\n\n if (!this.shouldRender) return;\n this.position.cancel();\n this.captureClosingSnapshot();\n this.closing = true;\n this.teardownListeners();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n @Watch('boundary')\n @Watch('side')\n @Watch('align')\n @Watch('sideOffset')\n @Watch('alignOffset')\n @Watch('menuWidth')\n onPositionInputChange() {\n if (this.open) this.position.schedule();\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (this.embedded && (event.key === 'Escape' || event.key === 'Tab')) return;\n if ((!this.shouldRender && !this.embedded) || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n return;\n }\n if (event.key !== 'Tab' || !this.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.close(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return this.triggerElement;\n }\n\n private get usesInternalTrigger(): boolean {\n return !this.embedded && !this.anchor && !this.anchorId;\n }\n\n private get viewportPad(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private setupListeners() {\n this.interaction.connect();\n this.position.observe();\n }\n\n private teardownListeners() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSnapshot();\n if (restoreFocus) this.interaction.focusAnchor();\n this.dsOpenChange.emit(false);\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n private toggleFromTrigger(event: MouseEvent) {\n if (this.open) {\n this.close();\n return;\n }\n this.pendingInitialFocusVisible = event.detail === 0;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private handleTriggerKeyDown(event: KeyboardEvent) {\n if (event.key !== 'ArrowDown' || this.open) return;\n event.preventDefault();\n this.pendingInitialFocusVisible = true;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closingSnapshot = null;\n this.closeTimer = null;\n this.dateModeByFilter = {};\n this.optionQueryByFilter = {};\n this.dsAfterClose.emit();\n }\n\n private captureClosingSnapshot() {\n if (this.closingSnapshot) return;\n this.closingSnapshot = {\n filters: this.filters.map(filter => ({\n ...filter,\n options: filter.options?.map(option => ({ ...option })),\n })),\n values: Object.fromEntries(\n Object.entries(this.values).map(([key, value]) => [\n key,\n Array.isArray(value) ? [...value] : value,\n ])\n ),\n matchModes: { ...this.matchModes },\n activeFilterId: this.activeFilterId,\n };\n }\n\n private focusInitialCategory() {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.filter-menu__category[tabindex=\"0\"]')?.focus();\n this.pendingInitialFocusVisible = false;\n });\n }\n\n private tabLeavesPopup(event: KeyboardEvent): boolean {\n return this.interaction.tabLeavesPopup(event);\n }\n\n private selectedFilter(filters: FilterMenuFilter[], activeFilterId: string | undefined) {\n return filters.find(filter => filter.id === activeFilterId) ?? filters[0];\n }\n\n private selectedCount(filter: FilterMenuFilter, values: FilterMenuValues): number {\n const value = values[filter.id];\n if (filter.kind === 'multiple') return Array.isArray(value) ? value.length : 0;\n if (filter.kind === 'boolean') return value === true ? 1 : 0;\n return typeof value === 'string' && value.length > 0 ? 1 : 0;\n }\n\n private totalSelected(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.reduce((total, filter) => total + this.selectedCount(filter, values), 0);\n }\n\n private activeFilterCount(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.filter(filter => this.selectedCount(filter, values) > 0).length;\n }\n\n private selectCategory(filterId: string, focus = false) {\n this.activeOptionIndex = 0;\n this.dsActiveFilterChange.emit(filterId);\n if (!focus) return;\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n });\n }\n\n private handleCategoryKeyDown(\n event: KeyboardEvent,\n filters: FilterMenuFilter[],\n filterId: string\n ) {\n const currentIndex = filters.findIndex(filter => filter.id === filterId);\n let nextIndex: number;\n if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % filters.length;\n else if (event.key === 'ArrowUp')\n nextIndex = (currentIndex - 1 + filters.length) % filters.length;\n else if (event.key === 'Home') nextIndex = 0;\n else if (event.key === 'End') nextIndex = filters.length - 1;\n else if (event.key === 'ArrowRight') {\n event.preventDefault();\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(\n '.filter-menu__options [role=\"option\"], .filter-menu__options [data-date-option], .filter-menu__options input'\n )\n ?.focus();\n });\n return;\n } else return;\n\n event.preventDefault();\n this.focusRingVisible = true;\n this.selectCategory(filters[nextIndex].id, true);\n }\n\n private handlePanelKeyDown(event: KeyboardEvent, filterId: string) {\n if (event.key !== 'ArrowLeft') return;\n event.preventDefault();\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n }\n\n private handleOptionKeyDown(\n event: KeyboardEvent,\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n index: number,\n values: FilterMenuValues,\n visibleOptions: FilterMenuOption[] = filter.options ?? []\n ) {\n if (event.key === 'ArrowLeft') {\n this.handlePanelKeyDown(event, filter.id);\n return;\n }\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectOption(filter, option, values);\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n event.preventDefault();\n const enabled = visibleOptions\n .map((candidate, optionIndex) => ({ candidate, optionIndex }))\n .filter(({ candidate }) => !candidate.isInactive)\n .map(({ optionIndex }) => optionIndex);\n if (!enabled.length) return;\n const current = enabled.indexOf(index);\n let next: number;\n if (event.key === 'Home') next = enabled[0];\n else if (event.key === 'End') next = enabled[enabled.length - 1];\n else {\n const direction = event.key === 'ArrowDown' ? 1 : -1;\n const start = current >= 0 ? current : direction === 1 ? -1 : 0;\n next = enabled[(start + direction + enabled.length) % enabled.length];\n }\n this.activeOptionIndex = next;\n this.focusRingVisible = true;\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n ?.focus();\n });\n }\n\n private optionMatchesQuery(option: FilterMenuOption, query: string): boolean {\n const normalized = query.trim().toLocaleLowerCase();\n if (!normalized) return true;\n return `${option.label} ${option.description ?? ''}`.toLocaleLowerCase().includes(normalized);\n }\n\n private visibleOptions(\n filter: FilterMenuFilter,\n query = this.optionQueryByFilter[filter.id] ?? ''\n ) {\n if (filter.kind === 'boolean') {\n const option = this.booleanOption(filter);\n return this.optionMatchesQuery(option, query) ? [option] : [];\n }\n return (filter.options ?? []).filter(option => this.optionMatchesQuery(option, query));\n }\n\n private booleanOption(filter: FilterMenuFilter): FilterMenuOption {\n return {\n label: filter.fieldLabel ?? filter.label,\n value: 'true',\n description: filter.description,\n };\n }\n\n private setOptionQuery(filter: FilterMenuFilter, value: string) {\n const visibleOptions = this.visibleOptions(filter, value);\n const firstEnabled = visibleOptions.findIndex(option => !option.isInactive);\n this.activeOptionIndex = firstEnabled;\n this.focusRingVisible = false;\n this.optionQueryByFilter = { ...this.optionQueryByFilter, [filter.id]: value };\n requestAnimationFrame(() => {\n const content = this.el.querySelector<HTMLElement>('.filter-menu__options-content');\n if (content) content.scrollTop = 0;\n this.position.schedule();\n });\n }\n\n private handleOptionSearchKeyDown(event: KeyboardEvent, filter: FilterMenuFilter) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n const visibleOptions = this.visibleOptions(filter);\n const enabled = visibleOptions\n .map((option, index) => ({ option, index }))\n .filter(({ option }) => !option.isInactive)\n .map(({ index }) => index);\n if (!enabled.length) return;\n event.preventDefault();\n const next = event.key === 'ArrowDown' ? enabled[0] : enabled[enabled.length - 1];\n this.activeOptionIndex = next;\n this.focusRingVisible = true;\n requestAnimationFrame(() => {\n this.el\n .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n ?.focus();\n });\n }\n\n private toggleMultiple(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n checked: boolean,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n const current = Array.isArray(values[filter.id]) ? (values[filter.id] as string[]) : [];\n const selected = new Set(current);\n if (checked) selected.add(option.value);\n else selected.delete(option.value);\n const ordered = (filter.options ?? [])\n .map(candidate => candidate.value)\n .filter(value => selected.has(value));\n this.changeValue({ filterId: filter.id, value: ordered });\n }\n\n private selectOption(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n if (filter.kind === 'single') {\n this.changeValue({ filterId: filter.id, value: option.value });\n return;\n }\n const selected = Array.isArray(values[filter.id])\n ? (values[filter.id] as string[]).includes(option.value)\n : false;\n this.toggleMultiple(filter, option, !selected, values);\n }\n\n private dateMode(filterId: string, value: FilterMenuValue | undefined): FilterMenuDateMode {\n return this.dateModeByFilter[filterId] ?? parseDateFilterValue(value)?.kind ?? 'relative';\n }\n\n private setDateMode(filter: FilterMenuFilter, mode: string) {\n if (mode !== 'range' && mode !== 'relative') return;\n this.dateModeByFilter = { ...this.dateModeByFilter, [filter.id]: mode };\n this.activeOptionIndex = 0;\n }\n\n private renderRelativeDateOptions(filter: FilterMenuFilter, value: FilterMenuValue | undefined) {\n const relativeFilter: FilterMenuFilter = {\n ...filter,\n kind: 'single',\n options: RELATIVE_DATE_OPTIONS,\n };\n return (\n <div\n class=\"filter-menu__date-relative ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"listbox\"\n aria-label=\"Relative date\"\n >\n {RELATIVE_DATE_OPTIONS.map((option, index) => (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-relative-${index}`}\n option={option}\n selected={value === option.value}\n active={index === this.activeOptionIndex}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={false}\n tabIndex={index === this.activeOptionIndex ? 0 : -1}\n onFocus={() => {\n this.activeOptionIndex = index;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleOptionKeyDown(event, relativeFilter, option, index, {\n [filter.id]: value,\n })\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeOptionIndex = index;\n }}\n onSelect={() => this.changeValue({ filterId: filter.id, value: option.value })}\n />\n ))}\n </div>\n );\n }\n\n private renderDateCalendar(filter: FilterMenuFilter, value: FilterMenuValue | undefined) {\n return (\n <ds-calendar\n selectionMode=\"range\"\n value={typeof value === 'string' ? value : ''}\n onDsChange={(event: CustomEvent<string>) => {\n event.stopPropagation();\n this.changeValue({ filterId: filter.id, value: event.detail });\n }}\n />\n );\n }\n\n private renderDateFilter(filter: FilterMenuFilter, value: FilterMenuValue | undefined) {\n const mode = this.dateMode(filter.id, value);\n return (\n <div class=\"filter-menu__date\">\n <header class=\"filter-menu__date-header ds-chrome-header ds-chrome-header--bounded\">\n <ds-tab-group\n ariaLabel=\"Date filter mode\"\n size=\"md\"\n width=\"fill\"\n hasContainer={false}\n value={mode}\n tabs={DATE_MODE_TABS}\n onDsChange={(event: CustomEvent<string>) => {\n event.stopPropagation();\n this.setDateMode(filter, event.detail);\n }}\n />\n </header>\n <div class=\"filter-menu__date-body\">\n {mode === 'relative'\n ? this.renderRelativeDateOptions(filter, value)\n : this.renderDateCalendar(filter, value)}\n </div>\n </div>\n );\n }\n\n private renderOptionSearch(filter: FilterMenuFilter) {\n return (\n <div class=\"filter-menu__option-search\">\n <ChoiceSearch\n size=\"md\"\n hasFocusBoundary={false}\n hasInteractionFill={false}\n value={this.optionQueryByFilter[filter.id] ?? ''}\n placeholder={this.searchPlaceholder}\n ariaLabel={`Search ${filter.label} options`}\n controls={`${this.generatedId}-${filter.id}-options`}\n inputRef={() => undefined}\n clearLabel={`Clear ${filter.label} search`}\n onValueChange={value => this.setOptionQuery(filter, value)}\n onKeyDown={(event: KeyboardEvent) => this.handleOptionSearchKeyDown(event, filter)}\n />\n </div>\n );\n }\n\n private renderNoOptionResults() {\n return (\n <div\n class=\"ds-choice-empty ds-empty-region\"\n role=\"option\"\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.noResultsText}\n aria-live=\"polite\"\n >\n <ds-empty-state body={this.noResultsText} />\n </div>\n );\n }\n\n private renderOptions(filter: FilterMenuFilter, values: FilterMenuValues) {\n const value = values[filter.id];\n if (filter.kind === 'multiple' || filter.kind === 'single') {\n const selected = Array.isArray(value) ? value : [];\n const allOptions = filter.options ?? [];\n const options = this.visibleOptions(filter);\n const usesSubtext = choiceListUsesSubtext(\n allOptions.map(option => ({\n label: option.label,\n value: option.value,\n subtext: option.description,\n }))\n );\n if (!options.length) return this.renderNoOptionResults();\n return options.map((option, index) => {\n const isSelected =\n filter.kind === 'multiple' ? selected.includes(option.value) : value === option.value;\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-${index}`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n isInactive: option.isInactive,\n }}\n selected={isSelected}\n active={index === this.activeOptionIndex}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n tabIndex={index === this.activeOptionIndex ? 0 : -1}\n leading={\n filter.kind === 'multiple' ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={isSelected} presentation />\n </span>\n ) : undefined\n }\n onFocus={() => {\n this.activeOptionIndex = index;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleOptionKeyDown(event, filter, option, index, values, options)\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeOptionIndex = index;\n }}\n onSelect={() => this.selectOption(filter, option, values)}\n />\n );\n });\n }\n\n if (filter.kind === 'boolean') {\n const [option] = this.visibleOptions(filter);\n if (!option) return this.renderNoOptionResults();\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-0`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n }}\n selected={value === true}\n active\n focusRingVisible={this.focusRingVisible}\n usesSubtext={Boolean(option.description)}\n tabIndex={0}\n leading={\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={value === true} presentation />\n </span>\n }\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'ArrowLeft') this.handlePanelKeyDown(event, filter.id);\n else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.changeValue({ filterId: filter.id, value: value !== true });\n }\n }}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.changeValue({ filterId: filter.id, value: value !== true })}\n />\n );\n }\n\n return this.renderDateFilter(filter, value);\n }\n\n private get captionIconOnly(): boolean {\n return (\n this.collapseLabel && this.captionCompact && this.usesInternalTrigger && Boolean(this.icon)\n );\n }\n\n private syncCaptionCompactObserver(): void {\n this.disconnectCaptionCompactObserver();\n if (!this.collapseLabel) {\n if (this.captionCompact) this.captionCompact = false;\n return;\n }\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private renderCaptionTooltip(trigger: VNode, label: string): VNode {\n if (!this.collapseLabel) return trigger;\n return (\n <ds-tooltip label={label} side=\"top\" size=\"sm\">\n {trigger}\n </ds-tooltip>\n );\n }\n\n private renderFooter(totalSelected: number, categoriesClear: boolean) {\n const appliedCount = this.totalSelected(this.filters, this.values);\n if (this.applyRequired && totalSelected === 0 && appliedCount === 0) return null;\n return (\n <div\n class={{\n 'filter-menu__footer': true,\n 'filter-menu__footer--categories-clear': categoriesClear,\n 'ds-choice-footer': true,\n }}\n aria-hidden={\n !this.applyRequired &&\n categoriesClear &&\n totalSelected === 0 &&\n !this.draftValues &&\n !this.draftModes\n ? 'true'\n : undefined\n }\n >\n <div class=\"ds-choice-footer__content ds-control--md\">\n {!categoriesClear ? (\n <ds-text\n class=\"ds-choice-footer__summary\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-live=\"polite\"\n >\n {totalSelected} selected\n </ds-text>\n ) : null}\n {this.applyRequired ? (\n <button\n class=\"filter-menu__apply ds-choice-footer__clear ds-text-action\"\n type=\"button\"\n disabled={!this.hasPendingChanges}\n onClick={() => this.applyDraft()}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n Apply\n </ds-text>\n </button>\n ) : null}\n {this.applyRequired && (totalSelected > 0 || appliedCount > 0) ? (\n <ds-text\n class=\"filter-menu__action-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ) : null}\n {totalSelected > 0 || (this.applyRequired && appliedCount > 0) ? (\n <button\n class=\"filter-menu__clear ds-choice-footer__clear ds-text-action\"\n type=\"button\"\n onClick={() => {\n if (this.applyRequired) {\n this.draftValues = {};\n this.draftModes = {};\n }\n this.dsClear.emit();\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.clearLabel}\n </ds-text>\n </button>\n ) : null}\n </div>\n </div>\n );\n }\n\n private renderMatchModeFooter(filter: FilterMenuFilter, matchModes: FilterMenuMatchModes) {\n const mode: FilterMenuMatchMode = matchModes[filter.id] === 'all' ? 'all' : 'any';\n const nextMode: FilterMenuMatchMode = mode === 'any' ? 'all' : 'any';\n return (\n <div class=\"filter-menu__match-mode-footer ds-choice-footer\">\n <div class=\"filter-menu__match-mode-content ds-choice-footer__content ds-control--md\">\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n Limit results to\n </ds-text>\n <button\n class=\"filter-menu__match-mode-toggle ds-text-action ds-focus-ring\"\n type=\"button\"\n aria-label={`Limit ${filter.label} results to ${nextMode} selected`}\n onClick={() => this.changeMode({ filterId: filter.id, mode: nextMode })}\n onKeyDown={(event: KeyboardEvent) => this.handlePanelKeyDown(event, filter.id)}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {mode}\n </ds-text>\n </button>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n selected\n </ds-text>\n </div>\n </div>\n );\n }\n\n render() {\n if (this.vertical)\n return (\n <Host class=\"filter-menu-vertical\">\n {this.filters.map(filter => (\n <details open key={filter.id} class=\"filter-menu-vertical__section\">\n <summary>\n <ds-icon class=\"filter-menu-vertical__chevron\" name=\"ChevronDown\" size=\"md\" />\n <ds-text variant=\"text-body-medium\" emphasis>\n {filter.label}\n </ds-text>\n </summary>\n {filter.kind !== 'date' && this.renderOptionSearch(filter)}\n <div\n id={`${this.generatedId}-${filter.id}-options`}\n role={filter.kind === 'date' ? undefined : 'listbox'}\n aria-label={filter.label}\n aria-multiselectable={filter.kind === 'multiple' ? 'true' : undefined}\n class=\"ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n >\n {this.renderOptions(filter, this.values)}\n </div>\n {filter.kind === 'multiple' && this.renderMatchModeFooter(filter, this.matchModes)}\n </details>\n ))}\n </Host>\n );\n const state =\n this.closing && this.closingSnapshot\n ? this.closingSnapshot\n : {\n filters: this.filters,\n values: this.draftValues ?? this.values,\n matchModes: this.draftModes ?? this.matchModes,\n activeFilterId: this.activeFilterId,\n };\n const activeFilter = this.selectedFilter(state.filters, state.activeFilterId);\n const totalSelected = this.totalSelected(state.filters, state.values);\n const activeFilterCount = this.activeFilterCount(state.filters, state.values);\n const hasActiveFilters = activeFilterCount > 0;\n const categoriesClearFooter = this.footerLayout === 'categories-clear';\n const label = `${this.triggerLabel}${\n this.showSelectedCount && hasActiveFilters ? ` · ${activeFilterCount}` : ''\n }`;\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const popupId = `${this.generatedId}-popup`;\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n width: this.menuWidth,\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host\n class={{\n 'filter-menu-host': true,\n 'filter-menu-host--external-trigger': !this.usesInternalTrigger,\n 'ds-select-trigger-host': this.usesInternalTrigger,\n [`ds-control--${this.size}`]: true,\n 'ds-table-caption-control': this.collapseLabel,\n 'ds-table-caption-control--compact': this.captionIconOnly,\n ...controlWidthClass(this.width),\n }}\n >\n {this.usesInternalTrigger\n ? this.renderCaptionTooltip(\n <button\n ref={element => {\n this.triggerElement = (element as HTMLButtonElement) ?? null;\n }}\n id={this.inputId ?? `${this.generatedId}-trigger`}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.activeFill && hasActiveFilters,\n 'trigger--expanded': this.open || this.closing,\n 'ds-interaction-fill--surface-open': this.open || this.closing,\n 'trigger--bordered': this.hasBorder,\n 'trigger--has-value': hasActiveFilters && !this.neutralTrigger,\n [`ds-control--${this.size}`]: true,\n }}\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-expanded={String(this.open)}\n aria-controls={this.open ? popupId : undefined}\n aria-label={this.ariaLabel ?? this.menuLabel}\n onClick={(event: MouseEvent) => this.toggleFromTrigger(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleTriggerKeyDown(event)}\n >\n {this.icon ? (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n ) : null}\n {this.captionIconOnly ? null : (\n <ds-text\n class=\"trigger__label-box trigger__label trigger__label-content ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n )}\n {this.captionIconOnly || !this.showIndicator ? null : (\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"ChevronDown\" size={iconSize} color=\"inherit\" />\n </span>\n )}\n </button>,\n this.captionIconOnly ? label : ''\n )\n : null}\n\n {this.shouldRender || this.embedded ? (\n <div\n id={popupId}\n popover={this.embedded ? undefined : 'manual'}\n class={{\n 'filter-menu-popup': true,\n 'ds-choice-popup': !this.embedded,\n 'data-preferences-embedded': this.embedded,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={this.embedded ? undefined : popupStyle}\n role=\"dialog\"\n aria-label={this.menuLabel}\n >\n {activeFilter ? (\n <div class=\"filter-menu\">\n <div class=\"filter-menu__body\">\n <div class=\"filter-menu__category-pane\">\n <div\n class=\"filter-menu__categories ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"tablist\"\n aria-label={this.categoriesLabel}\n aria-orientation=\"vertical\"\n >\n {state.filters.map(filter => {\n const active = filter.id === activeFilter.id;\n const count = this.selectedCount(filter, state.values);\n return (\n <div\n key={filter.id}\n class={{\n 'filter-menu__category-section': true,\n 'ds-choice-section--divided': Boolean(filter.divider),\n }}\n >\n <button\n id={`${this.generatedId}-${filter.id}-tab`}\n type=\"button\"\n role=\"tab\"\n data-filter-category={filter.id}\n class={{\n 'filter-menu__category': true,\n 'filter-menu__category--active': active,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n aria-selected={String(active)}\n aria-controls={`${this.generatedId}-panel`}\n tabIndex={active ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.selectCategory(filter.id)}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleCategoryKeyDown(event, state.filters, filter.id)\n }\n >\n <ds-text\n class=\"ds-choice-item__content ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {filter.label}\n </ds-text>\n {count > 0 && filter.kind === 'date' ? (\n <span\n class=\"filter-menu__category-dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge variant=\"dot\" hasRing={false} label=\"\" />\n </span>\n ) : count > 0 ? (\n <ds-tag\n class=\"ds-choice-item__tag ds-interaction-fill__content\"\n label={String(count)}\n size=\"sm\"\n intent=\"brand\"\n contrast=\"bold\"\n isInset\n rounded\n />\n ) : null}\n </button>\n </div>\n );\n })}\n </div>\n {categoriesClearFooter ? this.renderFooter(totalSelected, true) : null}\n </div>\n\n <div\n id={`${this.generatedId}-panel`}\n class={{\n 'filter-menu__options': true,\n 'filter-menu__options--date': activeFilter.kind === 'date',\n }}\n role=\"tabpanel\"\n aria-labelledby={`${this.generatedId}-${activeFilter.id}-tab`}\n >\n {activeFilter.kind === 'date' ? null : this.renderOptionSearch(activeFilter)}\n <div class=\"filter-menu__options-content\">\n {activeFilter.kind === 'date' ? (\n <div key={activeFilter.id} class=\"filter-menu__option-list--date\">\n {this.renderOptions(activeFilter, state.values)}\n </div>\n ) : (\n <div\n id={`${this.generatedId}-${activeFilter.id}-options`}\n key={activeFilter.id}\n class=\"filter-menu__option-list ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"listbox\"\n aria-label={activeFilter.label}\n aria-multiselectable={\n activeFilter.kind === 'multiple' ? 'true' : undefined\n }\n >\n {this.renderOptions(activeFilter, state.values)}\n </div>\n )}\n </div>\n {activeFilter.kind === 'multiple'\n ? this.renderMatchModeFooter(activeFilter, state.matchModes)\n : null}\n </div>\n </div>\n\n {!categoriesClearFooter && totalSelected > 0\n ? this.renderFooter(totalSelected, false)\n : null}\n </div>\n ) : null}\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
|
|
934
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n type VNode,\n Watch,\n} from '@stencil/core';\nimport {\n CONTROL_TEXT_VARIANT,\n DATE_FILTER_RELATIVE_PRESETS,\n controlWidthClass,\n dateFilterRelativeValue,\n parseDateFilterValue,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ControlWidth,\n} from '../../utils';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport type { AnchoredAlign, AnchoredSide } from '../../utils/anchored-position';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { ChoiceOptionRow, ChoiceSearch } from '../../utils/choice-list-parts';\nimport { choiceListUsesSubtext } from '../../utils/choice-list';\n\nexport type FilterMenuFilterKind = 'single' | 'multiple' | 'boolean' | 'date';\nexport type FilterMenuSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type FilterMenuWidth = ControlWidth;\nexport type FilterMenuFooterLayout = 'summary' | 'categories-clear';\nexport type FilterMenuMatchMode = 'any' | 'all';\ntype FilterMenuDateMode = 'range' | 'relative';\n\nexport interface FilterMenuOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport interface FilterMenuFilter {\n id: string;\n label: string;\n kind: FilterMenuFilterKind;\n options?: FilterMenuOption[];\n description?: string;\n fieldLabel?: string;\n /** Draw a standard menu-section divider after this category. */\n divider?: boolean;\n}\n\nexport type FilterMenuValue = string[] | boolean | string;\nexport type FilterMenuValues = Record<string, FilterMenuValue | undefined>;\nexport type FilterMenuMatchModes = Record<string, FilterMenuMatchMode | undefined>;\n\nexport interface FilterMenuChangeDetail {\n filterId: string;\n value: FilterMenuValue;\n}\n\nexport interface FilterMenuMatchModeChangeDetail {\n filterId: string;\n mode: FilterMenuMatchMode;\n}\n\nlet filterMenuId = 0;\n\nconst ICON_SIZE: Record<FilterMenuSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst DATE_MODE_TABS = [\n { id: 'relative', label: 'Relative' },\n { id: 'range', label: 'Range' },\n];\n\nconst RELATIVE_DATE_OPTIONS: FilterMenuOption[] = DATE_FILTER_RELATIVE_PRESETS.map(preset => ({\n label: preset.label,\n value: dateFilterRelativeValue(preset.value),\n}));\n\n@Component({\n tag: 'ds-filter-menu',\n styleUrl: 'FilterMenu.css',\n scoped: true,\n})\nexport class FilterMenu {\n @Element() el!: HTMLElement;\n\n /** Controlled popup visibility. */\n /** Render content inside a parent-owned surface; the parent owns dismissal and positioning. */\n @Prop() embedded: boolean = false;\n /** Stacked, collapsible sections for a configuration panel. */\n @Prop() vertical: boolean = false;\n @Prop({ mutable: true }) open: boolean = false;\n /** Select trigger text. */\n @Prop() triggerLabel: string = 'Filters';\n /** Append the active-filter count to the visible trigger label. */\n @Prop() showSelectedCount: boolean = true;\n /** Optional select trigger prefix icon name. */\n @Prop() icon: string | undefined = 'Filters';\n /** Select trigger density. */\n @Prop() size: FilterMenuSize = 'md';\n /** Select trigger width fit. */\n @Prop() width: FilterMenuWidth = 'hug';\n /** Show the surface-aware inset border around the select trigger. */\n /** Show the trigger chevron. */\n @Prop() showIndicator: boolean = true;\n @Prop() hasBorder: boolean = true;\n /** Show selected interaction fill when one or more criteria are active. */\n /** Keep trigger colors neutral when a value is selected. */\n @Prop() neutralTrigger: boolean = false;\n /** Stage filter edits until Apply is pressed. */\n @Prop() applyRequired: boolean = false;\n @State() private draftValues?: FilterMenuValues;\n @State() private draftModes?: FilterMenuMatchModes;\n\n private changeValue(detail: FilterMenuChangeDetail): void {\n if (this.applyRequired)\n this.draftValues = { ...(this.draftValues ?? this.values), [detail.filterId]: detail.value };\n else this.dsChange.emit(detail);\n }\n private changeMode(detail: FilterMenuMatchModeChangeDetail): void {\n if (this.applyRequired)\n this.draftModes = { ...(this.draftModes ?? this.matchModes), [detail.filterId]: detail.mode };\n else this.dsMatchModeChange.emit(detail);\n }\n private get hasPendingChanges(): boolean {\n const normalize = (value: FilterMenuValue | undefined) =>\n Array.isArray(value) ? (value.length ? [...value].sort() : undefined) : value || undefined;\n const values = this.draftValues ?? this.values;\n const modes = this.draftModes ?? this.matchModes;\n return this.filters.some(\n filter =>\n JSON.stringify(normalize(values[filter.id])) !==\n JSON.stringify(normalize(this.values[filter.id])) ||\n (modes[filter.id] ?? 'any') !== (this.matchModes[filter.id] ?? 'any')\n );\n }\n private applyDraft(): void {\n if (!this.hasPendingChanges) return;\n if (this.draftValues && Object.keys(this.draftValues).length === 0) {\n this.dsClear.emit();\n } else if (this.draftValues) {\n for (const id of new Set([...Object.keys(this.values), ...Object.keys(this.draftValues)])) {\n if (JSON.stringify(this.values[id]) !== JSON.stringify(this.draftValues[id]))\n this.dsChange.emit({\n filterId: id,\n value:\n this.draftValues[id] ??\n (Array.isArray(this.values[id])\n ? []\n : typeof this.values[id] === 'boolean'\n ? false\n : ''),\n });\n }\n }\n if (this.draftModes) {\n for (const filterId of new Set([\n ...Object.keys(this.matchModes),\n ...Object.keys(this.draftModes),\n ])) {\n const mode = this.draftModes[filterId] ?? 'any';\n if ((this.matchModes[filterId] ?? 'any') !== mode)\n this.dsMatchModeChange.emit({ filterId, mode });\n }\n }\n }\n @Prop() activeFill: boolean = false;\n /**\n * Opt into table-caption icon-only chrome below 900px. The trigger omits its\n * visible label and chevron; keep an accessible name via aria-label.\n */\n @Prop({ reflect: true }) collapseLabel: boolean = false;\n /** ID applied to the internal select trigger. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name for the internal select trigger. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Product-owned filter categories and option definitions. */\n @Prop() filters: FilterMenuFilter[] = [];\n /** Controlled values keyed by filter id. */\n @Prop() values: FilterMenuValues = {};\n /** Controlled any/all match mode keyed by multiple-choice filter id. Defaults to any. */\n @Prop() matchModes: FilterMenuMatchModes = {};\n /** Controlled category shown in the option pane. */\n @Prop() activeFilterId: string | undefined;\n /** External trigger element to position against. */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element used for positioning and focus return. */\n @Prop() anchorId: string | undefined;\n /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | undefined;\n /** Preferred popup side; collision handling may flip it. */\n @Prop() side: AnchoredSide = 'bottom';\n /** Cross-axis alignment against the trigger. */\n @Prop() align: AnchoredAlign = 'end';\n /** Gap between trigger and popup — number in px or a TokoMo length. */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number in px or a TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n /** Popup width. It remains clamped to its collision boundary by the component recipe. */\n @Prop() menuWidth: string = TOKEN_CSS_LENGTHS.menuWidthLg;\n /** Accessible name for the non-modal filter dialog. */\n @Prop() menuLabel: string = 'Filters';\n /** Accessible name for the category tab list. */\n @Prop() categoriesLabel: string = 'Filter categories';\n /** Footer action and date-clear accessible label. */\n @Prop() clearLabel: string = 'Clear';\n /** Placeholder shown in each non-date option search header. */\n @Prop() searchPlaceholder: string = 'Search';\n /** Empty-state text shown when an option search has no matches. */\n @Prop() noResultsText: string = 'No results';\n /** Footer recipe: full-width selected summary or reserved category-pane Clear action. */\n @Prop() footerLayout: FilterMenuFooterLayout = 'summary';\n /** Show a visible focus ring on initial entry after keyboard activation. */\n @Prop() initialFocusVisible: boolean = false;\n\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusRingVisible = false;\n @State() private activeOptionIndex = 0;\n @State() private captionCompact = false;\n @State() private dateModeByFilter: Record<string, FilterMenuDateMode> = {};\n @State() private optionQueryByFilter: Record<string, string> = {};\n\n /** Requests a controlled value replacement without closing the popup. */\n @Event() dsChange!: EventEmitter<FilterMenuChangeDetail>;\n /** Requests that the consumer clear every filter value. */\n @Event() dsClear!: EventEmitter<void>;\n /** Requests a controlled any/all mode replacement for a multiple-choice filter. */\n @Event() dsMatchModeChange!: EventEmitter<FilterMenuMatchModeChangeDetail>;\n /** Requests a controlled active-category replacement. */\n @Event() dsActiveFilterChange!: EventEmitter<string>;\n /** Requests that the controlled popup close. */\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted whenever internal select-trigger activation changes popup visibility. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted after exit motion and rendered popup removal complete. */\n @Event() dsAfterClose!: EventEmitter<void>;\n\n private readonly generatedId = `ds-filter-menu-${++filterMenuId}`;\n private triggerElement: HTMLButtonElement | null = null;\n private pendingInitialFocusVisible = false;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n private readonly tasks = new ConnectionTasks(() => this.el.isConnected);\n private closingSnapshot: {\n filters: FilterMenuFilter[];\n values: FilterMenuValues;\n matchModes: FilterMenuMatchModes;\n activeFilterId: string | undefined;\n } | null = null;\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const anchorRect = anchor.getBoundingClientRect();\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: this.side,\n align: this.align,\n sideOffsetPx: resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: resolveCssLengthPx(this.alignOffset, 0),\n sectionInsetPx,\n }),\n viewportPadPx: this.viewportPad,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor, this.boundary),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.filter-menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n connectedCallback() {\n if (!this.hasLoaded) return;\n this.syncCaptionCompactObserver();\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.disconnectCaptionCompactObserver();\n this.position.unobserve();\n this.teardownListeners();\n this.shouldRender = false;\n this.closing = false;\n this.positionReady = false;\n this.closingSnapshot = null;\n this.pendingInitialFocusVisible = false;\n if (!this.open) {\n this.dateModeByFilter = {};\n this.optionQueryByFilter = {};\n }\n }\n\n @Watch('collapseLabel')\n onCollapseLabelChange() {\n this.syncCaptionCompactObserver();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (!isOpen) {\n this.draftValues = undefined;\n this.draftModes = undefined;\n }\n if (this.embedded || !this.hasLoaded || !this.el.isConnected) return;\n if (isOpen) {\n this.teardownListeners();\n this.closingSnapshot = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.pendingInitialFocusVisible || this.initialFocusVisible;\n this.setupListeners();\n this.position.schedule(() => this.focusInitialCategory());\n return;\n }\n\n if (!this.shouldRender || this.closing) return;\n this.position.cancel();\n this.captureClosingSnapshot();\n this.closing = true;\n this.teardownListeners();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n @Watch('boundary')\n @Watch('side')\n @Watch('align')\n @Watch('sideOffset')\n @Watch('alignOffset')\n @Watch('menuWidth')\n onPositionInputChange() {\n if (this.open && this.el.isConnected) this.position.schedule();\n }\n\n @Listen('keydown')\n handleKeyDown(event: KeyboardEvent) {\n if (this.embedded && (event.key === 'Escape' || event.key === 'Tab')) return;\n if ((!this.shouldRender && !this.embedded) || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.close();\n return;\n }\n if (event.key !== 'Tab' || !this.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.close(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return this.triggerElement;\n }\n\n private get usesInternalTrigger(): boolean {\n return !this.embedded && !this.anchor && !this.anchorId;\n }\n\n private get viewportPad(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthLg, TOKEN_DEFAULTS.menuWidthLg);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private setupListeners() {\n this.interaction.connect();\n this.position.observe();\n }\n\n private teardownListeners() {\n this.tasks.cancel();\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSnapshot();\n if (restoreFocus) this.interaction.focusAnchor();\n this.dsOpenChange.emit(false);\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n private toggleFromTrigger(event: MouseEvent) {\n if (this.open) {\n this.close();\n return;\n }\n this.pendingInitialFocusVisible = event.detail === 0;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private handleTriggerKeyDown(event: KeyboardEvent) {\n if (event.key !== 'ArrowDown' || this.open) return;\n event.preventDefault();\n this.pendingInitialFocusVisible = true;\n this.open = true;\n this.dsOpenChange.emit(true);\n this.onOpenChange(true);\n }\n\n private finishClose() {\n if (this.open || !this.closing || !this.el.isConnected) return;\n const popup = this.el.querySelector<HTMLElement>('.filter-menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closingSnapshot = null;\n this.closeTimer = null;\n this.dateModeByFilter = {};\n this.optionQueryByFilter = {};\n this.dsAfterClose.emit();\n }\n\n private captureClosingSnapshot() {\n if (this.closingSnapshot) return;\n this.closingSnapshot = {\n filters: this.filters.map(filter => ({\n ...filter,\n options: filter.options?.map(option => ({ ...option })),\n })),\n values: Object.fromEntries(\n Object.entries(this.values).map(([key, value]) => [\n key,\n Array.isArray(value) ? [...value] : value,\n ])\n ),\n matchModes: { ...this.matchModes },\n activeFilterId: this.activeFilterId,\n };\n }\n\n private focusInitialCategory() {\n this.tasks.frame(() => {\n this.el.querySelector<HTMLElement>('.filter-menu__category[tabindex=\"0\"]')?.focus();\n this.pendingInitialFocusVisible = false;\n });\n }\n\n private tabLeavesPopup(event: KeyboardEvent): boolean {\n return this.interaction.tabLeavesPopup(event);\n }\n\n private selectedFilter(filters: FilterMenuFilter[], activeFilterId: string | undefined) {\n return filters.find(filter => filter.id === activeFilterId) ?? filters[0];\n }\n\n private selectedCount(filter: FilterMenuFilter, values: FilterMenuValues): number {\n const value = values[filter.id];\n if (filter.kind === 'multiple') return Array.isArray(value) ? value.length : 0;\n if (filter.kind === 'boolean') return value === true ? 1 : 0;\n return typeof value === 'string' && value.length > 0 ? 1 : 0;\n }\n\n private totalSelected(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.reduce((total, filter) => total + this.selectedCount(filter, values), 0);\n }\n\n private activeFilterCount(filters: FilterMenuFilter[], values: FilterMenuValues): number {\n return filters.filter(filter => this.selectedCount(filter, values) > 0).length;\n }\n\n private selectCategory(filterId: string, focus = false) {\n this.activeOptionIndex = 0;\n this.dsActiveFilterChange.emit(filterId);\n if (!focus) return;\n this.tasks.frame(() => {\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n });\n }\n\n private handleCategoryKeyDown(\n event: KeyboardEvent,\n filters: FilterMenuFilter[],\n filterId: string\n ) {\n const currentIndex = filters.findIndex(filter => filter.id === filterId);\n let nextIndex: number;\n if (event.key === 'ArrowDown') nextIndex = (currentIndex + 1) % filters.length;\n else if (event.key === 'ArrowUp')\n nextIndex = (currentIndex - 1 + filters.length) % filters.length;\n else if (event.key === 'Home') nextIndex = 0;\n else if (event.key === 'End') nextIndex = filters.length - 1;\n else if (event.key === 'ArrowRight') {\n event.preventDefault();\n this.tasks.frame(() => {\n this.el\n .querySelector<HTMLElement>(\n '.filter-menu__options [role=\"option\"], .filter-menu__options [data-date-option], .filter-menu__options input'\n )\n ?.focus();\n });\n return;\n } else return;\n\n event.preventDefault();\n this.focusRingVisible = true;\n this.selectCategory(filters[nextIndex].id, true);\n }\n\n private handlePanelKeyDown(event: KeyboardEvent, filterId: string) {\n if (event.key !== 'ArrowLeft') return;\n event.preventDefault();\n this.el.querySelector<HTMLElement>(`[data-filter-category=\"${filterId}\"]`)?.focus();\n }\n\n private handleOptionKeyDown(\n event: KeyboardEvent,\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n index: number,\n values: FilterMenuValues,\n visibleOptions: FilterMenuOption[] = filter.options ?? []\n ) {\n if (event.key === 'ArrowLeft') {\n this.handlePanelKeyDown(event, filter.id);\n return;\n }\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.selectOption(filter, option, values);\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;\n event.preventDefault();\n const enabled = visibleOptions\n .map((candidate, optionIndex) => ({ candidate, optionIndex }))\n .filter(({ candidate }) => !candidate.isInactive)\n .map(({ optionIndex }) => optionIndex);\n if (!enabled.length) return;\n const current = enabled.indexOf(index);\n let next: number;\n if (event.key === 'Home') next = enabled[0];\n else if (event.key === 'End') next = enabled[enabled.length - 1];\n else {\n const direction = event.key === 'ArrowDown' ? 1 : -1;\n const start = current >= 0 ? current : direction === 1 ? -1 : 0;\n next = enabled[(start + direction + enabled.length) % enabled.length];\n }\n this.activeOptionIndex = next;\n this.focusRingVisible = true;\n this.tasks.frame(() => {\n this.el\n .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n ?.focus();\n });\n }\n\n private optionMatchesQuery(option: FilterMenuOption, query: string): boolean {\n const normalized = query.trim().toLocaleLowerCase();\n if (!normalized) return true;\n return `${option.label} ${option.description ?? ''}`.toLocaleLowerCase().includes(normalized);\n }\n\n private visibleOptions(\n filter: FilterMenuFilter,\n query = this.optionQueryByFilter[filter.id] ?? ''\n ) {\n if (filter.kind === 'boolean') {\n const option = this.booleanOption(filter);\n return this.optionMatchesQuery(option, query) ? [option] : [];\n }\n return (filter.options ?? []).filter(option => this.optionMatchesQuery(option, query));\n }\n\n private booleanOption(filter: FilterMenuFilter): FilterMenuOption {\n return {\n label: filter.fieldLabel ?? filter.label,\n value: 'true',\n description: filter.description,\n };\n }\n\n private setOptionQuery(filter: FilterMenuFilter, value: string) {\n const visibleOptions = this.visibleOptions(filter, value);\n const firstEnabled = visibleOptions.findIndex(option => !option.isInactive);\n this.activeOptionIndex = firstEnabled;\n this.focusRingVisible = false;\n this.optionQueryByFilter = { ...this.optionQueryByFilter, [filter.id]: value };\n this.tasks.frame(() => {\n const content = this.el.querySelector<HTMLElement>('.filter-menu__options-content');\n if (content) content.scrollTop = 0;\n this.position.schedule();\n });\n }\n\n private handleOptionSearchKeyDown(event: KeyboardEvent, filter: FilterMenuFilter) {\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n const visibleOptions = this.visibleOptions(filter);\n const enabled = visibleOptions\n .map((option, index) => ({ option, index }))\n .filter(({ option }) => !option.isInactive)\n .map(({ index }) => index);\n if (!enabled.length) return;\n event.preventDefault();\n const next = event.key === 'ArrowDown' ? enabled[0] : enabled[enabled.length - 1];\n this.activeOptionIndex = next;\n this.focusRingVisible = true;\n this.tasks.frame(() => {\n this.el\n .querySelector<HTMLElement>(`#${this.generatedId}-${filter.id}-option-${next}`)\n ?.focus();\n });\n }\n\n private toggleMultiple(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n checked: boolean,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n const current = Array.isArray(values[filter.id]) ? (values[filter.id] as string[]) : [];\n const selected = new Set(current);\n if (checked) selected.add(option.value);\n else selected.delete(option.value);\n const ordered = (filter.options ?? [])\n .map(candidate => candidate.value)\n .filter(value => selected.has(value));\n this.changeValue({ filterId: filter.id, value: ordered });\n }\n\n private selectOption(\n filter: FilterMenuFilter,\n option: FilterMenuOption,\n values: FilterMenuValues\n ) {\n if (option.isInactive) return;\n if (filter.kind === 'single') {\n this.changeValue({ filterId: filter.id, value: option.value });\n return;\n }\n const selected = Array.isArray(values[filter.id])\n ? (values[filter.id] as string[]).includes(option.value)\n : false;\n this.toggleMultiple(filter, option, !selected, values);\n }\n\n private dateMode(filterId: string, value: FilterMenuValue | undefined): FilterMenuDateMode {\n return this.dateModeByFilter[filterId] ?? parseDateFilterValue(value)?.kind ?? 'relative';\n }\n\n private setDateMode(filter: FilterMenuFilter, mode: string) {\n if (mode !== 'range' && mode !== 'relative') return;\n this.dateModeByFilter = { ...this.dateModeByFilter, [filter.id]: mode };\n this.activeOptionIndex = 0;\n }\n\n private renderRelativeDateOptions(filter: FilterMenuFilter, value: FilterMenuValue | undefined) {\n const relativeFilter: FilterMenuFilter = {\n ...filter,\n kind: 'single',\n options: RELATIVE_DATE_OPTIONS,\n };\n return (\n <div\n class=\"filter-menu__date-relative ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"listbox\"\n aria-label=\"Relative date\"\n >\n {RELATIVE_DATE_OPTIONS.map((option, index) => (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-relative-${index}`}\n option={option}\n selected={value === option.value}\n active={index === this.activeOptionIndex}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={false}\n tabIndex={index === this.activeOptionIndex ? 0 : -1}\n onFocus={() => {\n this.activeOptionIndex = index;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleOptionKeyDown(event, relativeFilter, option, index, {\n [filter.id]: value,\n })\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeOptionIndex = index;\n }}\n onSelect={() => this.changeValue({ filterId: filter.id, value: option.value })}\n />\n ))}\n </div>\n );\n }\n\n private renderDateCalendar(filter: FilterMenuFilter, value: FilterMenuValue | undefined) {\n return (\n <ds-calendar\n selectionMode=\"range\"\n value={typeof value === 'string' ? value : ''}\n onDsChange={(event: CustomEvent<string>) => {\n event.stopPropagation();\n this.changeValue({ filterId: filter.id, value: event.detail });\n }}\n />\n );\n }\n\n private renderDateFilter(filter: FilterMenuFilter, value: FilterMenuValue | undefined) {\n const mode = this.dateMode(filter.id, value);\n return (\n <div class=\"filter-menu__date\">\n <header class=\"filter-menu__date-header ds-chrome-header ds-chrome-header--bounded\">\n <ds-tab-group\n ariaLabel=\"Date filter mode\"\n size=\"md\"\n width=\"fill\"\n hasContainer={false}\n value={mode}\n tabs={DATE_MODE_TABS}\n onDsChange={(event: CustomEvent<string>) => {\n event.stopPropagation();\n this.setDateMode(filter, event.detail);\n }}\n />\n </header>\n <div class=\"filter-menu__date-body\">\n {mode === 'relative'\n ? this.renderRelativeDateOptions(filter, value)\n : this.renderDateCalendar(filter, value)}\n </div>\n </div>\n );\n }\n\n private renderOptionSearch(filter: FilterMenuFilter) {\n return (\n <div class=\"filter-menu__option-search\">\n <ChoiceSearch\n size=\"md\"\n hasFocusBoundary={false}\n hasInteractionFill={false}\n value={this.optionQueryByFilter[filter.id] ?? ''}\n placeholder={this.searchPlaceholder}\n ariaLabel={`Search ${filter.label} options`}\n controls={`${this.generatedId}-${filter.id}-options`}\n inputRef={() => undefined}\n clearLabel={`Clear ${filter.label} search`}\n onValueChange={value => this.setOptionQuery(filter, value)}\n onKeyDown={(event: KeyboardEvent) => this.handleOptionSearchKeyDown(event, filter)}\n />\n </div>\n );\n }\n\n private renderNoOptionResults() {\n return (\n <div\n class=\"ds-choice-empty ds-empty-region\"\n role=\"option\"\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.noResultsText}\n aria-live=\"polite\"\n >\n <ds-empty-state body={this.noResultsText} />\n </div>\n );\n }\n\n private renderOptions(filter: FilterMenuFilter, values: FilterMenuValues) {\n const value = values[filter.id];\n if (filter.kind === 'multiple' || filter.kind === 'single') {\n const selected = Array.isArray(value) ? value : [];\n const allOptions = filter.options ?? [];\n const options = this.visibleOptions(filter);\n const usesSubtext = choiceListUsesSubtext(\n allOptions.map(option => ({\n label: option.label,\n value: option.value,\n subtext: option.description,\n }))\n );\n if (!options.length) return this.renderNoOptionResults();\n return options.map((option, index) => {\n const isSelected =\n filter.kind === 'multiple' ? selected.includes(option.value) : value === option.value;\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-${index}`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n isInactive: option.isInactive,\n }}\n selected={isSelected}\n active={index === this.activeOptionIndex}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n tabIndex={index === this.activeOptionIndex ? 0 : -1}\n leading={\n filter.kind === 'multiple' ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={isSelected} presentation />\n </span>\n ) : undefined\n }\n onFocus={() => {\n this.activeOptionIndex = index;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleOptionKeyDown(event, filter, option, index, values, options)\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeOptionIndex = index;\n }}\n onSelect={() => this.selectOption(filter, option, values)}\n />\n );\n });\n }\n\n if (filter.kind === 'boolean') {\n const [option] = this.visibleOptions(filter);\n if (!option) return this.renderNoOptionResults();\n return (\n <ChoiceOptionRow\n size=\"md\"\n id={`${this.generatedId}-${filter.id}-option-0`}\n option={{\n label: option.label,\n value: option.value,\n subtext: option.description,\n }}\n selected={value === true}\n active\n focusRingVisible={this.focusRingVisible}\n usesSubtext={Boolean(option.description)}\n tabIndex={0}\n leading={\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox label=\"\" size=\"md\" checked={value === true} presentation />\n </span>\n }\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'ArrowLeft') this.handlePanelKeyDown(event, filter.id);\n else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n this.changeValue({ filterId: filter.id, value: value !== true });\n }\n }}\n onHover={() => {\n this.focusRingVisible = false;\n }}\n onSelect={() => this.changeValue({ filterId: filter.id, value: value !== true })}\n />\n );\n }\n\n return this.renderDateFilter(filter, value);\n }\n\n private get captionIconOnly(): boolean {\n return (\n this.collapseLabel && this.captionCompact && this.usesInternalTrigger && Boolean(this.icon)\n );\n }\n\n private syncCaptionCompactObserver(): void {\n this.disconnectCaptionCompactObserver();\n if (!this.el.isConnected) return;\n if (!this.collapseLabel) {\n if (this.captionCompact) this.captionCompact = false;\n return;\n }\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private renderCaptionTooltip(trigger: VNode, label: string): VNode {\n if (!this.collapseLabel) return trigger;\n return (\n <ds-tooltip label={label} side=\"top\" size=\"sm\">\n {trigger}\n </ds-tooltip>\n );\n }\n\n private renderFooter(totalSelected: number, categoriesClear: boolean) {\n const appliedCount = this.totalSelected(this.filters, this.values);\n if (this.applyRequired && totalSelected === 0 && appliedCount === 0) return null;\n return (\n <div\n class={{\n 'filter-menu__footer': true,\n 'filter-menu__footer--categories-clear': categoriesClear,\n 'ds-choice-footer': true,\n }}\n aria-hidden={\n !this.applyRequired &&\n categoriesClear &&\n totalSelected === 0 &&\n !this.draftValues &&\n !this.draftModes\n ? 'true'\n : undefined\n }\n >\n <div class=\"ds-choice-footer__content ds-control--md\">\n {!categoriesClear ? (\n <ds-text\n class=\"ds-choice-footer__summary\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-live=\"polite\"\n >\n {totalSelected} selected\n </ds-text>\n ) : null}\n {this.applyRequired ? (\n <button\n class=\"filter-menu__apply ds-choice-footer__clear ds-text-action\"\n type=\"button\"\n disabled={!this.hasPendingChanges}\n onClick={() => this.applyDraft()}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n Apply\n </ds-text>\n </button>\n ) : null}\n {this.applyRequired && (totalSelected > 0 || appliedCount > 0) ? (\n <ds-text\n class=\"filter-menu__action-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ) : null}\n {totalSelected > 0 || (this.applyRequired && appliedCount > 0) ? (\n <button\n class=\"filter-menu__clear ds-choice-footer__clear ds-text-action\"\n type=\"button\"\n onClick={() => {\n if (this.applyRequired) {\n this.draftValues = {};\n this.draftModes = {};\n }\n this.dsClear.emit();\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.clearLabel}\n </ds-text>\n </button>\n ) : null}\n </div>\n </div>\n );\n }\n\n private renderMatchModeFooter(filter: FilterMenuFilter, matchModes: FilterMenuMatchModes) {\n const mode: FilterMenuMatchMode = matchModes[filter.id] === 'all' ? 'all' : 'any';\n const nextMode: FilterMenuMatchMode = mode === 'any' ? 'all' : 'any';\n return (\n <div class=\"filter-menu__match-mode-footer ds-choice-footer\">\n <div class=\"filter-menu__match-mode-content ds-choice-footer__content ds-control--md\">\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n Limit results to\n </ds-text>\n <button\n class=\"filter-menu__match-mode-toggle ds-text-action ds-focus-ring\"\n type=\"button\"\n aria-label={`Limit ${filter.label} results to ${nextMode} selected`}\n onClick={() => this.changeMode({ filterId: filter.id, mode: nextMode })}\n onKeyDown={(event: KeyboardEvent) => this.handlePanelKeyDown(event, filter.id)}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {mode}\n </ds-text>\n </button>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n selected\n </ds-text>\n </div>\n </div>\n );\n }\n\n render() {\n if (this.vertical)\n return (\n <Host class=\"filter-menu-vertical\">\n {this.filters.map(filter => (\n <details open key={filter.id} class=\"filter-menu-vertical__section\">\n <summary>\n <ds-icon class=\"filter-menu-vertical__chevron\" name=\"ChevronDown\" size=\"md\" />\n <ds-text variant=\"text-body-medium\" emphasis>\n {filter.label}\n </ds-text>\n </summary>\n {filter.kind !== 'date' && this.renderOptionSearch(filter)}\n <div\n id={`${this.generatedId}-${filter.id}-options`}\n role={filter.kind === 'date' ? undefined : 'listbox'}\n aria-label={filter.label}\n aria-multiselectable={filter.kind === 'multiple' ? 'true' : undefined}\n class=\"ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n >\n {this.renderOptions(filter, this.values)}\n </div>\n {filter.kind === 'multiple' && this.renderMatchModeFooter(filter, this.matchModes)}\n </details>\n ))}\n </Host>\n );\n const state =\n this.closing && this.closingSnapshot\n ? this.closingSnapshot\n : {\n filters: this.filters,\n values: this.draftValues ?? this.values,\n matchModes: this.draftModes ?? this.matchModes,\n activeFilterId: this.activeFilterId,\n };\n const activeFilter = this.selectedFilter(state.filters, state.activeFilterId);\n const totalSelected = this.totalSelected(state.filters, state.values);\n const activeFilterCount = this.activeFilterCount(state.filters, state.values);\n const hasActiveFilters = activeFilterCount > 0;\n const categoriesClearFooter = this.footerLayout === 'categories-clear';\n const label = `${this.triggerLabel}${\n this.showSelectedCount && hasActiveFilters ? ` · ${activeFilterCount}` : ''\n }`;\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const popupId = `${this.generatedId}-popup`;\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n width: this.menuWidth,\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host\n class={{\n 'filter-menu-host': true,\n 'filter-menu-host--external-trigger': !this.usesInternalTrigger,\n 'ds-select-trigger-host': this.usesInternalTrigger,\n [`ds-control--${this.size}`]: true,\n 'ds-table-caption-control': this.collapseLabel,\n 'ds-table-caption-control--compact': this.captionIconOnly,\n ...controlWidthClass(this.width),\n }}\n >\n {this.usesInternalTrigger\n ? this.renderCaptionTooltip(\n <button\n ref={element => {\n this.triggerElement = (element as HTMLButtonElement) ?? null;\n }}\n id={this.inputId ?? `${this.generatedId}-trigger`}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.activeFill && hasActiveFilters,\n 'trigger--expanded': this.open || this.closing,\n 'ds-interaction-fill--surface-open': this.open || this.closing,\n 'trigger--bordered': this.hasBorder,\n 'trigger--has-value': hasActiveFilters && !this.neutralTrigger,\n [`ds-control--${this.size}`]: true,\n }}\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-expanded={String(this.open)}\n aria-controls={this.open ? popupId : undefined}\n aria-label={this.ariaLabel ?? this.menuLabel}\n onClick={(event: MouseEvent) => this.toggleFromTrigger(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleTriggerKeyDown(event)}\n >\n {this.icon ? (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n ) : null}\n {this.captionIconOnly ? null : (\n <ds-text\n class=\"trigger__label-box trigger__label trigger__label-content ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n )}\n {this.captionIconOnly || !this.showIndicator ? null : (\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"ChevronDown\" size={iconSize} color=\"inherit\" />\n </span>\n )}\n </button>,\n this.captionIconOnly ? label : ''\n )\n : null}\n\n {this.shouldRender || this.embedded ? (\n <div\n id={popupId}\n popover={this.embedded ? undefined : 'manual'}\n class={{\n 'filter-menu-popup': true,\n 'ds-choice-popup': !this.embedded,\n 'data-preferences-embedded': this.embedded,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={this.embedded ? undefined : popupStyle}\n role=\"dialog\"\n aria-label={this.menuLabel}\n >\n {activeFilter ? (\n <div class=\"filter-menu\">\n <div class=\"filter-menu__body\">\n <div class=\"filter-menu__category-pane\">\n <div\n class=\"filter-menu__categories ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"tablist\"\n aria-label={this.categoriesLabel}\n aria-orientation=\"vertical\"\n >\n {state.filters.map(filter => {\n const active = filter.id === activeFilter.id;\n const count = this.selectedCount(filter, state.values);\n return (\n <div\n key={filter.id}\n class={{\n 'filter-menu__category-section': true,\n 'ds-choice-section--divided': Boolean(filter.divider),\n }}\n >\n <button\n id={`${this.generatedId}-${filter.id}-tab`}\n type=\"button\"\n role=\"tab\"\n data-filter-category={filter.id}\n class={{\n 'filter-menu__category': true,\n 'filter-menu__category--active': active,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n aria-selected={String(active)}\n aria-controls={`${this.generatedId}-panel`}\n tabIndex={active ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.selectCategory(filter.id)}\n onKeyDown={(event: KeyboardEvent) =>\n this.handleCategoryKeyDown(event, state.filters, filter.id)\n }\n >\n <ds-text\n class=\"ds-choice-item__content ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {filter.label}\n </ds-text>\n {count > 0 && filter.kind === 'date' ? (\n <span\n class=\"filter-menu__category-dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge variant=\"dot\" hasRing={false} label=\"\" />\n </span>\n ) : count > 0 ? (\n <ds-tag\n class=\"ds-choice-item__tag ds-interaction-fill__content\"\n label={String(count)}\n size=\"sm\"\n intent=\"brand\"\n contrast=\"bold\"\n isInset\n rounded\n />\n ) : null}\n </button>\n </div>\n );\n })}\n </div>\n {categoriesClearFooter ? this.renderFooter(totalSelected, true) : null}\n </div>\n\n <div\n id={`${this.generatedId}-panel`}\n class={{\n 'filter-menu__options': true,\n 'filter-menu__options--date': activeFilter.kind === 'date',\n }}\n role=\"tabpanel\"\n aria-labelledby={`${this.generatedId}-${activeFilter.id}-tab`}\n >\n {activeFilter.kind === 'date' ? null : this.renderOptionSearch(activeFilter)}\n <div class=\"filter-menu__options-content\">\n {activeFilter.kind === 'date' ? (\n <div key={activeFilter.id} class=\"filter-menu__option-list--date\">\n {this.renderOptions(activeFilter, state.values)}\n </div>\n ) : (\n <div\n id={`${this.generatedId}-${activeFilter.id}-options`}\n key={activeFilter.id}\n class=\"filter-menu__option-list ds-choice-list ds-chrome-column ds-chrome-space--sm\"\n role=\"listbox\"\n aria-label={activeFilter.label}\n aria-multiselectable={\n activeFilter.kind === 'multiple' ? 'true' : undefined\n }\n >\n {this.renderOptions(activeFilter, state.values)}\n </div>\n )}\n </div>\n {activeFilter.kind === 'multiple'\n ? this.renderMatchModeFooter(activeFilter, state.matchModes)\n : null}\n </div>\n </div>\n\n {!categoriesClearFooter && totalSelected > 0\n ? this.renderFooter(totalSelected, false)\n : null}\n </div>\n ) : null}\n </div>\n ) : null}\n </Host>\n );\n }\n}\n",
|
|
935
935
|
"type": "registry:ui"
|
|
936
936
|
}
|
|
937
937
|
]
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
"Built-in glyph lookup accepts exact canonical IcoMo export names only. The system, flag, and map catalogs are selected by which one owns the name, never by its prefix, so system glyphs such as Flag, FlagFilled, MapPage, and MapPin resolve correctly.",
|
|
64
64
|
"A cache miss lazy-loads into a fixed-size box without layout shift. Unknown names, failed loads, and rejected markup leave that box empty.",
|
|
65
65
|
"Critical first-paint glyphs may be pre-registered before rendering; all resolved glyphs share a global cache.",
|
|
66
|
-
"Every SVG is validated at the render boundary and
|
|
66
|
+
"Every SVG is validated at the render boundary and constructed with namespaced DOM APIs so app-registered markup cannot bypass executable-content or external-reference checks.",
|
|
67
|
+
"Custom registered glyphs must be static and self-contained. Local gradients and clipping are supported, and narrow inline paint styles preserve IcoMo flag colors. Embedded stylesheets, animation and external resources are not supported."
|
|
67
68
|
],
|
|
68
69
|
"responsiveBehavior": [
|
|
69
70
|
"The owning component or layout chooses icon size and any responsive changes.",
|
|
@@ -236,12 +237,12 @@
|
|
|
236
237
|
"files": [
|
|
237
238
|
{
|
|
238
239
|
"path": "src/wc/components/Icon/Icon.css",
|
|
239
|
-
"content": "/* ── Base icon ───────────────────────────────────────────────────────────── */\n\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n line-height: 0;\n color: inherit;\n /* Explicit size so the SVG (width/height: 100% via JS) has a defined container */\n width: var(--_icon-sz, var(--dimension-iconography-md));\n height: var(--_icon-sz, var(--dimension-iconography-md));\n}\n\n.icon__svg {\n display: contents;\n line-height: 0;\n}\n\n/* SVG width/height are set via inline style in updateSvg() because Stencil's\n scoped CSS cannot reach
|
|
240
|
+
"content": "/* ── Base icon ───────────────────────────────────────────────────────────── */\n\n.icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n line-height: 0;\n color: inherit;\n /* Explicit size so the SVG (width/height: 100% via JS) has a defined container */\n width: var(--_icon-sz, var(--dimension-iconography-md));\n height: var(--_icon-sz, var(--dimension-iconography-md));\n}\n\n.icon__svg {\n display: contents;\n line-height: 0;\n}\n\n/* SVG width/height are set via inline style in updateSvg() because Stencil's\n scoped CSS cannot reach constructed SVG nodes (they lack the sc-* class). */\n\n/* ── Sizes ───────────────────────────────────────────────────────────────── */\n\n.icon--size-xs {\n --_icon-sz: var(--dimension-iconography-xs);\n}\n.icon--size-sm {\n --_icon-sz: var(--dimension-iconography-sm);\n}\n.icon--size-md {\n --_icon-sz: var(--dimension-iconography-md);\n}\n.icon--size-lg {\n --_icon-sz: var(--dimension-iconography-lg);\n}\n.icon--size-xl {\n --_icon-sz: var(--dimension-iconography-xl);\n}\n.icon--size-2xl {\n --_icon-sz: var(--dimension-iconography-2xl);\n}\n.icon--size-3xl {\n --_icon-sz: var(--dimension-iconography-3xl);\n}\n\n/* ── Colors — hierarchy ──────────────────────────────────────────────────── */\n\n.icon--color-primary {\n color: var(--color-foreground-primary);\n}\n.icon--color-secondary {\n color: var(--color-foreground-secondary);\n}\n.icon--color-tertiary {\n color: var(--color-foreground-tertiary);\n}\n.icon--color-quaternary {\n color: var(--color-foreground-quaternary);\n}\n.icon--color-inherit {\n color: inherit;\n}\n\n/* ── Colors — intent (bold contrast default) ─────────────────────────────── */\n\n.icon--color-brand {\n color: var(--color-foreground-bold-brand);\n}\n.icon--color-negative {\n color: var(--color-foreground-bold-negative);\n}\n.icon--color-positive {\n color: var(--color-foreground-bold-positive);\n}\n.icon--color-warning {\n color: var(--color-foreground-bold-warning);\n}\n.icon--color-caution {\n color: var(--color-foreground-bold-caution);\n}\n.icon--color-ai {\n color: var(--color-foreground-bold-ai);\n}\n.icon--color-guide {\n color: var(--color-foreground-bold-guide);\n}\n.icon--color-neutral {\n color: var(--color-foreground-bold-neutral);\n}\n\n/* ── Colors — intent faint ───────────────────────────────────────────────── */\n\n.icon--color-faint-brand {\n color: var(--color-foreground-faint-brand);\n}\n.icon--color-faint-negative {\n color: var(--color-foreground-faint-negative);\n}\n.icon--color-faint-positive {\n color: var(--color-foreground-faint-positive);\n}\n.icon--color-faint-warning {\n color: var(--color-foreground-faint-warning);\n}\n.icon--color-faint-caution {\n color: var(--color-foreground-faint-caution);\n}\n.icon--color-faint-ai {\n color: var(--color-foreground-faint-ai);\n}\n.icon--color-faint-neutral {\n color: var(--color-foreground-faint-neutral);\n}\n\n/* ── Colors — intent medium ─────────────────────────────────────────────── */\n\n.icon--color-medium-brand {\n color: var(--color-foreground-medium-brand);\n}\n.icon--color-medium-negative {\n color: var(--color-foreground-medium-negative);\n}\n.icon--color-medium-positive {\n color: var(--color-foreground-medium-positive);\n}\n.icon--color-medium-warning {\n color: var(--color-foreground-medium-warning);\n}\n.icon--color-medium-caution {\n color: var(--color-foreground-medium-caution);\n}\n.icon--color-medium-ai {\n color: var(--color-foreground-medium-ai);\n}\n.icon--color-medium-neutral {\n color: var(--color-foreground-medium-neutral);\n}\n\n/* ── Colors — intent bold (explicit) ────────────────────────────────────── */\n\n.icon--color-bold-brand {\n color: var(--color-foreground-bold-brand);\n}\n.icon--color-bold-negative {\n color: var(--color-foreground-bold-negative);\n}\n.icon--color-bold-positive {\n color: var(--color-foreground-bold-positive);\n}\n.icon--color-bold-warning {\n color: var(--color-foreground-bold-warning);\n}\n.icon--color-bold-caution {\n color: var(--color-foreground-bold-caution);\n}\n.icon--color-bold-ai {\n color: var(--color-foreground-bold-ai);\n}\n.icon--color-bold-neutral {\n color: var(--color-foreground-bold-neutral);\n}\n\n/* ── Colors — intent strong ──────────────────────────────────────────────── */\n\n.icon--color-strong-brand {\n color: var(--color-foreground-strong-brand);\n}\n.icon--color-strong-negative {\n color: var(--color-foreground-strong-negative);\n}\n.icon--color-strong-positive {\n color: var(--color-foreground-strong-positive);\n}\n.icon--color-strong-warning {\n color: var(--color-foreground-strong-warning);\n}\n.icon--color-strong-caution {\n color: var(--color-foreground-strong-caution);\n}\n.icon--color-strong-ai {\n color: var(--color-foreground-strong-ai);\n}\n.icon--color-strong-neutral {\n color: var(--color-foreground-strong-neutral);\n}\n\n/* ── Colors — contextual ────────────────────────────────────────────────── */\n\n.icon--color-on-strong {\n color: var(--color-foreground-on-strong-background-primary);\n}\n.icon--color-on-bold {\n color: var(--color-foreground-on-bold-background-primary);\n}\n",
|
|
240
241
|
"type": "registry:ui"
|
|
241
242
|
},
|
|
242
243
|
{
|
|
243
244
|
"path": "src/wc/components/Icon/Icon.tsx",
|
|
244
|
-
"content": "import { Component, Prop, Element, State, Watch, h, Host } from '@stencil/core';\nimport { flagIconLoaders } from './flag-icon-catalog';\nimport { systemIconLoaders } from './system-icon-catalog';\nimport { mapIconLoaders } from './map-icon-catalog';\nimport { iconCache, iconCacheKey, resolveIconCategory } from './icon-cache';\nimport type { IconCategory } from './icon-cache';\nimport { parseIconSvg } from './icon-svg';\n\n/** One lazy-loader catalog per IcoMo category, keyed so routing stays a lookup. */\nconst ICON_LOADERS: Record<IconCategory, Record<string, () => Promise<string>>> = {\n system: systemIconLoaders,\n flag: flagIconLoaders,\n map: mapIconLoaders,\n};\n\nexport type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';\n\nexport type IconColorToken =\n // Hierarchy\n | 'primary'\n | 'secondary'\n | 'tertiary'\n | 'quaternary'\n // Intent — bold contrast (default)\n | 'brand'\n | 'negative'\n | 'positive'\n | 'warning'\n | 'caution'\n | 'ai'\n | 'guide'\n | 'neutral'\n // Intent — faint\n | 'faint-brand'\n | 'faint-negative'\n | 'faint-positive'\n | 'faint-warning'\n | 'faint-caution'\n | 'faint-ai'\n | 'faint-neutral'\n // Intent — medium\n | 'medium-brand'\n | 'medium-negative'\n | 'medium-positive'\n | 'medium-warning'\n | 'medium-caution'\n | 'medium-ai'\n | 'medium-neutral'\n // Intent — bold (explicit)\n | 'bold-brand'\n | 'bold-negative'\n | 'bold-positive'\n | 'bold-warning'\n | 'bold-caution'\n | 'bold-ai'\n | 'bold-neutral'\n // Intent — strong\n | 'strong-brand'\n | 'strong-negative'\n | 'strong-positive'\n | 'strong-warning'\n | 'strong-caution'\n | 'strong-ai'\n | 'strong-neutral'\n // Contextual\n | 'on-strong'\n | 'on-bold'\n // Pass-through\n | 'inherit';\n\nexport type IconColor = IconColorToken | `var(--${string})`;\n\n@Component({\n tag: 'ds-icon',\n styleUrl: 'Icon.css',\n scoped: true,\n})\nexport class Icon {\n @Element() el!: HTMLElement;\n\n /** Exact canonical IcoMo export name, including the `Flag` prefix for flags. */\n @Prop() name: string = '';\n /** Iconography size token. Maps to `--dimension-iconography-{size}`. Default `md` = 20 px. */\n @Prop() size: IconSize = 'md';\n /**\n * Semantic foreground color token, or a raw CSS var reference. `tertiary`\n * and the still-fainter `quaternary` are restricted to icons inside genuinely\n * inactive/disabled UI or to purely decorative icons; informative icons must\n * retain sufficient contrast.\n */\n @Prop() color: IconColor = 'inherit';\n /** Accessible label. Sets `role=\"img\"` and `aria-label`. Omit for decorative icons. */\n @Prop() label: string | undefined;\n\n /** Resolved SVG markup — set synchronously on cache hit, async after a lazy load. */\n @State() private svg: string = '';\n\n /** Guards against out-of-order async resolutions when `name`/`flag` change quickly. */\n private loadToken = 0;\n\n /**\n * Cache hit → render synchronously (pre-registered via `registerIcons` or\n * previously loaded). Miss → fire the per-icon lazy loader; the glyph pops in\n * when the (tiny, per-icon) chunk resolves and stays cached for every other\n * ds-icon instance.\n */\n private resolveSvg() {\n const category = resolveIconCategory(this.name);\n const key = iconCacheKey(this.name, category);\n const token = ++this.loadToken;\n\n const cached = iconCache().get(key);\n if (cached !== undefined) {\n this.svg = cached;\n return;\n }\n\n // Own-key lookup only: names resolve by exact canonical IcoMo export key —\n // never meta.json aliases (not in the maps) and never inherited prototype\n // keys ('constructor', 'toString', …) that a bare index access would hit.\n const loaders = ICON_LOADERS[category];\n const loader = Object.prototype.hasOwnProperty.call(loaders, this.name)\n ? loaders[this.name]\n : undefined;\n if (!loader) {\n this.svg = '';\n return;\n }\n\n loader()\n .then(svg => {\n iconCache().set(key, svg);\n if (token === this.loadToken) this.svg = svg;\n })\n .catch(() => {\n if (token === this.loadToken) this.svg = '';\n });\n }\n\n componentWillLoad() {\n // Fire-and-forget: do not return the promise — a cache miss must not block\n // the parent render tree on a network fetch.\n this.resolveSvg();\n }\n\n /** Last markup injected into the DOM — skips redundant re-parse on unrelated re-renders. */\n private renderedSvg: string | null = null;\n\n private updateSvg() {\n const container = this.el.querySelector<HTMLElement>('.icon__svg');\n if (!container) return;\n if (this.svg === this.renderedSvg) return;\n this.renderedSvg = this.svg;\n\n // Validate +
|
|
245
|
+
"content": "import { Component, Prop, Element, State, Watch, h, Host } from '@stencil/core';\nimport { flagIconLoaders } from './flag-icon-catalog';\nimport { systemIconLoaders } from './system-icon-catalog';\nimport { mapIconLoaders } from './map-icon-catalog';\nimport { iconCache, iconCacheKey, resolveIconCategory } from './icon-cache';\nimport type { IconCategory } from './icon-cache';\nimport { parseIconSvg } from './icon-svg';\n\n/** One lazy-loader catalog per IcoMo category, keyed so routing stays a lookup. */\nconst ICON_LOADERS: Record<IconCategory, Record<string, () => Promise<string>>> = {\n system: systemIconLoaders,\n flag: flagIconLoaders,\n map: mapIconLoaders,\n};\n\nexport type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';\n\nexport type IconColorToken =\n // Hierarchy\n | 'primary'\n | 'secondary'\n | 'tertiary'\n | 'quaternary'\n // Intent — bold contrast (default)\n | 'brand'\n | 'negative'\n | 'positive'\n | 'warning'\n | 'caution'\n | 'ai'\n | 'guide'\n | 'neutral'\n // Intent — faint\n | 'faint-brand'\n | 'faint-negative'\n | 'faint-positive'\n | 'faint-warning'\n | 'faint-caution'\n | 'faint-ai'\n | 'faint-neutral'\n // Intent — medium\n | 'medium-brand'\n | 'medium-negative'\n | 'medium-positive'\n | 'medium-warning'\n | 'medium-caution'\n | 'medium-ai'\n | 'medium-neutral'\n // Intent — bold (explicit)\n | 'bold-brand'\n | 'bold-negative'\n | 'bold-positive'\n | 'bold-warning'\n | 'bold-caution'\n | 'bold-ai'\n | 'bold-neutral'\n // Intent — strong\n | 'strong-brand'\n | 'strong-negative'\n | 'strong-positive'\n | 'strong-warning'\n | 'strong-caution'\n | 'strong-ai'\n | 'strong-neutral'\n // Contextual\n | 'on-strong'\n | 'on-bold'\n // Pass-through\n | 'inherit';\n\nexport type IconColor = IconColorToken | `var(--${string})`;\n\n@Component({\n tag: 'ds-icon',\n styleUrl: 'Icon.css',\n scoped: true,\n})\nexport class Icon {\n @Element() el!: HTMLElement;\n\n /** Exact canonical IcoMo export name, including the `Flag` prefix for flags. */\n @Prop() name: string = '';\n /** Iconography size token. Maps to `--dimension-iconography-{size}`. Default `md` = 20 px. */\n @Prop() size: IconSize = 'md';\n /**\n * Semantic foreground color token, or a raw CSS var reference. `tertiary`\n * and the still-fainter `quaternary` are restricted to icons inside genuinely\n * inactive/disabled UI or to purely decorative icons; informative icons must\n * retain sufficient contrast.\n */\n @Prop() color: IconColor = 'inherit';\n /** Accessible label. Sets `role=\"img\"` and `aria-label`. Omit for decorative icons. */\n @Prop() label: string | undefined;\n\n /** Resolved SVG markup — set synchronously on cache hit, async after a lazy load. */\n @State() private svg: string = '';\n\n /** Guards against out-of-order async resolutions when `name`/`flag` change quickly. */\n private loadToken = 0;\n\n /**\n * Cache hit → render synchronously (pre-registered via `registerIcons` or\n * previously loaded). Miss → fire the per-icon lazy loader; the glyph pops in\n * when the (tiny, per-icon) chunk resolves and stays cached for every other\n * ds-icon instance.\n */\n private resolveSvg() {\n const category = resolveIconCategory(this.name);\n const key = iconCacheKey(this.name, category);\n const token = ++this.loadToken;\n\n const cached = iconCache().get(key);\n if (cached !== undefined) {\n this.svg = cached;\n return;\n }\n\n // Own-key lookup only: names resolve by exact canonical IcoMo export key —\n // never meta.json aliases (not in the maps) and never inherited prototype\n // keys ('constructor', 'toString', …) that a bare index access would hit.\n const loaders = ICON_LOADERS[category];\n const loader = Object.prototype.hasOwnProperty.call(loaders, this.name)\n ? loaders[this.name]\n : undefined;\n if (!loader) {\n this.svg = '';\n return;\n }\n\n loader()\n .then(svg => {\n iconCache().set(key, svg);\n if (token === this.loadToken) this.svg = svg;\n })\n .catch(() => {\n if (token === this.loadToken) this.svg = '';\n });\n }\n\n componentWillLoad() {\n // Fire-and-forget: do not return the promise — a cache miss must not block\n // the parent render tree on a network fetch.\n this.resolveSvg();\n }\n\n /** Last markup injected into the DOM — skips redundant re-parse on unrelated re-renders. */\n private renderedSvg: string | null = null;\n\n private updateSvg() {\n const container = this.el.querySelector<HTMLElement>('.icon__svg');\n if (!container) return;\n if (this.svg === this.renderedSvg) return;\n this.renderedSvg = this.svg;\n\n // Validate + construct namespaced DOM nodes without a markup sink. Keeps ds-icon\n // Trusted-Types compatible and rejects executable/foreign content in\n // glyph strings (registerIcons accepts app-provided markup).\n const svg = this.svg ? parseIconSvg(this.svg, this.el.ownerDocument) : null;\n if (!svg) {\n container.replaceChildren();\n return;\n }\n\n // Stencil's scoped CSS cannot reach injected elements (no sc-* class).\n // Apply width/height as inline styles so sizing works regardless of scope class.\n svg.removeAttribute('width');\n svg.removeAttribute('height');\n // Prevent SVG from being natively focusable (Firefox/IE default) while\n // sitting inside an aria-hidden parent — fixes aria-hidden-focus violation.\n svg.setAttribute('focusable', 'false');\n svg.setAttribute('aria-hidden', 'true');\n svg.style.display = 'block';\n svg.style.flexShrink = '0';\n svg.style.width = '100%';\n svg.style.height = '100%';\n container.replaceChildren(svg);\n }\n\n @Watch('name')\n onIconChange() {\n this.resolveSvg();\n }\n\n componentDidRender() {\n this.updateSvg();\n }\n\n render() {\n const isCustomColor = typeof this.color === 'string' && this.color.startsWith('var(--');\n\n const cls: Record<string, boolean> = {\n icon: true,\n [`icon--size-${this.size}`]: true,\n [`icon--color-${this.color}`]: !!this.color && !isCustomColor && this.color !== 'inherit',\n 'icon--color-inherit': this.color === 'inherit',\n };\n\n const style = isCustomColor ? { color: this.color as string } : undefined;\n\n return (\n <Host style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>\n <span\n class={cls}\n style={style}\n role={this.label ? 'img' : 'presentation'}\n aria-label={this.label}\n aria-hidden={!this.label ? 'true' : undefined}\n >\n <span class=\"icon__svg\" />\n </span>\n </Host>\n );\n }\n}\n",
|
|
245
246
|
"type": "registry:ui"
|
|
246
247
|
},
|
|
247
248
|
{
|
|
@@ -251,12 +252,12 @@
|
|
|
251
252
|
},
|
|
252
253
|
{
|
|
253
254
|
"path": "src/wc/components/Icon/icon-cache.ts",
|
|
254
|
-
"content": "/**\n * Shared SVG glyph cache for ds-icon.\n *\n * Keyed on a global symbol so every module instance (the compiled Stencil chunk\n * inside dist/components and any direct source import, e.g. via `@ds-mo/ui/utils`)\n * reads and writes the same cache. Without this, `registerIcons` called from an\n * app would fill a different Map than the one ds-icon resolves from.\n */\nimport { flagIconLoaders } from './flag-icon-catalog';\nimport { mapIconLoaders } from './map-icon-catalog';\n\nconst CACHE_KEY = Symbol.for('ds-mo.icon-svg-cache');\n\ntype IconCacheMap = Map<string, string>;\n\ntype GlobalWithIconCache = { [CACHE_KEY]?: IconCacheMap };\n\n/** IcoMo ships each glyph in exactly one category; the catalogs mirror that split. */\nexport type IconCategory = 'system' | 'flag' | 'map';\n\nexport function iconCacheKey(name: string, category: IconCategory): string {\n return `${category}:${name}`;\n}\n\nfunction hasOwn(loaders: Record<string, unknown>, name: string): boolean {\n return Object.prototype.hasOwnProperty.call(loaders, name);\n}\n\n/**\n * Pick the catalog a name belongs to by own-key membership — never by prefix.\n *\n * A prefix test looks equivalent and is not. IcoMo ships `Flag`/`FlagFilled` as\n * *system* icons (a generic flag glyph, not a country) and `MapNavigation`,\n * `MapPage`, `MapPin`, `MapStreet` as system icons too, while every genuine\n * flag/map export carries the category prefix. So `startsWith('Flag')` or\n * `startsWith('Map')` misroutes those six to a catalog that does not contain\n * them, the own-key lookup misses, and ds-icon renders nothing with no error.\n *\n * Only the two prefixed catalogs are consulted; anything else is system. That\n * keeps the system catalog — 400 loaders, by far the largest — out of the\n * `@ds-mo/ui/utils` entry, which exists to hand apps `registerIcons`.\n *\n * Test order is not a tie-break: `generate-icon-catalog.mjs` fails the build if\n * IcoMo ever ships one name in two categories, so at most one can match.\n *\n * Names in no catalog resolve as system, which is what lets an app register a\n * glyph of its own under a custom name and have ds-icon read it back.\n */\nexport function resolveIconCategory(name: string): IconCategory {\n if (hasOwn(flagIconLoaders, name)) return 'flag';\n if (hasOwn(mapIconLoaders, name)) return 'map';\n return 'system';\n}\n\nexport function iconCache(): IconCacheMap {\n const g = globalThis as GlobalWithIconCache;\n return (g[CACHE_KEY] ??= new Map());\n}\n\n/**\n * Pre-register glyphs so ds-icon renders them synchronously — no lazy-chunk\n * fetch, no first-paint pop-in. Import the SVG strings from `@ds-mo/icons`\n * in the app (statically, so they ship in the app's own bundle):\n *\n * ```ts\n * import { Bell } from '@ds-mo/icons/svg/Bell';\n * import { FlagUnitedStates } from '@ds-mo/icons/svg/flags/FlagUnitedStates';\n * import { MapGeofence } from '@ds-mo/icons/svg/map/MapGeofence';\n * import { registerIcons } from '@ds-mo/ui/utils';\n *\n * registerIcons({ Bell });\n * registerIcons({ FlagUnitedStates });\n * registerIcons({ MapGeofence });\n * ```\n *\n * The category comes from the name, exactly as ds-icon derives it, so a\n * registration is always readable by the element that needs it. Glyphs under a\n * name IcoMo does not ship — an app's own marker artwork, say — register as\n * system and resolve the same way.\n *\n * There is deliberately no category override. ds-icon has no such override when\n * it reads, so forcing a different category on write could only produce an\n * entry nothing ever reads.\n *\n * Icons that are not registered still work — they lazy-load on first render\n * and stay cached afterwards.\n */\nexport function registerIcons(icons: Record<string, string>): void {\n const cache = iconCache();\n for (const [name, svg] of Object.entries(icons)) {\n cache.set(iconCacheKey(name, resolveIconCategory(name)), svg);\n }\n}\n",
|
|
255
|
+
"content": "/**\n * Shared SVG glyph cache for ds-icon.\n *\n * Keyed on a global symbol so every module instance (the compiled Stencil chunk\n * inside dist/components and any direct source import, e.g. via `@ds-mo/ui/utils`)\n * reads and writes the same cache. Without this, `registerIcons` called from an\n * app would fill a different Map than the one ds-icon resolves from.\n */\nimport { flagIconLoaders } from './flag-icon-catalog';\nimport { mapIconLoaders } from './map-icon-catalog';\n\nconst CACHE_KEY = Symbol.for('ds-mo.icon-svg-cache');\n\ntype IconCacheMap = Map<string, string>;\n\ntype GlobalWithIconCache = { [CACHE_KEY]?: IconCacheMap };\n\n/** IcoMo ships each glyph in exactly one category; the catalogs mirror that split. */\nexport type IconCategory = 'system' | 'flag' | 'map';\n\nexport function iconCacheKey(name: string, category: IconCategory): string {\n return `${category}:${name}`;\n}\n\nfunction hasOwn(loaders: Record<string, unknown>, name: string): boolean {\n return Object.prototype.hasOwnProperty.call(loaders, name);\n}\n\n/**\n * Pick the catalog a name belongs to by own-key membership — never by prefix.\n *\n * A prefix test looks equivalent and is not. IcoMo ships `Flag`/`FlagFilled` as\n * *system* icons (a generic flag glyph, not a country) and `MapNavigation`,\n * `MapPage`, `MapPin`, `MapStreet` as system icons too, while every genuine\n * flag/map export carries the category prefix. So `startsWith('Flag')` or\n * `startsWith('Map')` misroutes those six to a catalog that does not contain\n * them, the own-key lookup misses, and ds-icon renders nothing with no error.\n *\n * Only the two prefixed catalogs are consulted; anything else is system. That\n * keeps the system catalog — 400 loaders, by far the largest — out of the\n * `@ds-mo/ui/utils` entry, which exists to hand apps `registerIcons`.\n *\n * Test order is not a tie-break: `generate-icon-catalog.mjs` fails the build if\n * IcoMo ever ships one name in two categories, so at most one can match.\n *\n * Names in no catalog resolve as system, which is what lets an app register a\n * glyph of its own under a custom name and have ds-icon read it back.\n */\nexport function resolveIconCategory(name: string): IconCategory {\n if (hasOwn(flagIconLoaders, name)) return 'flag';\n if (hasOwn(mapIconLoaders, name)) return 'map';\n return 'system';\n}\n\nexport function iconCache(): IconCacheMap {\n const g = globalThis as GlobalWithIconCache;\n return (g[CACHE_KEY] ??= new Map());\n}\n\n/**\n * Pre-register glyphs so ds-icon renders them synchronously — no lazy-chunk\n * fetch, no first-paint pop-in. Import the SVG strings from `@ds-mo/icons`\n * in the app (statically, so they ship in the app's own bundle):\n *\n * ```ts\n * import { Bell } from '@ds-mo/icons/svg/Bell';\n * import { FlagUnitedStates } from '@ds-mo/icons/svg/flags/FlagUnitedStates';\n * import { MapGeofence } from '@ds-mo/icons/svg/map/MapGeofence';\n * import { registerIcons } from '@ds-mo/ui/utils';\n *\n * registerIcons({ Bell });\n * registerIcons({ FlagUnitedStates });\n * registerIcons({ MapGeofence });\n * ```\n *\n * The category comes from the name, exactly as ds-icon derives it, so a\n * registration is always readable by the element that needs it. Glyphs under a\n * name IcoMo does not ship — an app's own marker artwork, say — register as\n * system and resolve the same way.\n *\n * Custom glyphs must be static and self-contained. Shapes, local gradients,\n * clipping and narrow inline paint styles are supported; scripts, embedded\n * stylesheets, animation, images and external resources are rejected. Validation\n * happens when rendering, including for pre-registered strings. Unsupported\n * markup preserves the icon's empty fixed-size box.\n *\n * There is deliberately no category override. ds-icon has no such override when\n * it reads, so forcing a different category on write could only produce an\n * entry nothing ever reads.\n *\n * Icons that are not registered still work — they lazy-load on first render\n * and stay cached afterwards.\n */\nexport function registerIcons(icons: Record<string, string>): void {\n const cache = iconCache();\n for (const [name, svg] of Object.entries(icons)) {\n cache.set(iconCacheKey(name, resolveIconCategory(name)), svg);\n }\n}\n",
|
|
255
256
|
"type": "registry:ui"
|
|
256
257
|
},
|
|
257
258
|
{
|
|
258
259
|
"path": "src/wc/components/Icon/icon-svg.ts",
|
|
259
|
-
"content": "
|
|
260
|
+
"content": "import sax, { type QualifiedTag } from 'sax';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\nconst XLINK_NS = 'http://www.w3.org/1999/xlink';\nconst ID = /^[A-Za-z_][\\w.-]*$/;\nconst LOCAL_PAINT = /^url\\(\\s*(['\"]?)#([A-Za-z_][\\w.-]*)\\1\\s*\\)$/;\nconst PAINT =\n /^(?:none|currentColor|transparent|[a-z]+|#[\\da-f]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\\([-+.\\d\\s,%/]+\\)|color\\((?:display-p3|srgb|srgb-linear|a98-rgb|prophoto-rgb|rec2020|xyz|xyz-d50|xyz-d65)\\s+[-+.\\d\\s%/]+\\))$/i;\nconst ELEMENTS = new Set([\n 'svg',\n 'g',\n 'defs',\n 'path',\n 'rect',\n 'circle',\n 'ellipse',\n 'line',\n 'polyline',\n 'polygon',\n 'linearGradient',\n 'radialGradient',\n 'stop',\n 'clipPath',\n 'mask',\n 'use',\n 'title',\n 'desc',\n]);\nconst PAINT_ATTRIBUTES = new Set(['fill', 'stroke', 'color', 'stop-color']);\nconst PRESENTATION = new Set([\n ...PAINT_ATTRIBUTES,\n 'opacity',\n 'fill-opacity',\n 'stroke-opacity',\n 'stop-opacity',\n 'fill-rule',\n 'clip-rule',\n 'stroke-width',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-miterlimit',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'vector-effect',\n]);\nconst ATTRIBUTES = new Set([\n ...PRESENTATION,\n 'id',\n 'style',\n 'xmlns',\n 'xmlns:xlink',\n 'href',\n 'xlink:href',\n 'viewBox',\n 'preserveAspectRatio',\n 'transform',\n 'x',\n 'y',\n 'x1',\n 'y1',\n 'x2',\n 'y2',\n 'cx',\n 'cy',\n 'r',\n 'rx',\n 'ry',\n 'width',\n 'height',\n 'd',\n 'points',\n 'pathLength',\n 'gradientUnits',\n 'gradientTransform',\n 'spreadMethod',\n 'offset',\n 'fx',\n 'fy',\n 'fr',\n 'clipPathUnits',\n 'maskUnits',\n 'maskContentUnits',\n 'clip-path',\n 'mask',\n]);\n\nexport interface IconSvgNode {\n name: string;\n attributes: Record<string, string>;\n styles: [string, string][];\n children: IconSvgNode[];\n text: string;\n}\n\nfunction paintReference(value: string): string | undefined {\n return value.match(LOCAL_PAINT)?.[2];\n}\n\nfunction validPresentation(name: string, value: string): boolean {\n if (PAINT_ATTRIBUTES.has(name)) return PAINT.test(value) || !!paintReference(value);\n // Static presentation only: no escapes, comments, variables or priorities.\n return /^[\\w\\s.,%+-]+$/.test(value);\n}\n\nfunction parseStyles(value: string): [string, string][] | null {\n const entries: [string, string][] = [];\n for (const declaration of value.split(';')) {\n if (!declaration.trim()) continue;\n const colon = declaration.indexOf(':');\n const name = declaration.slice(0, colon).trim();\n const paint = declaration.slice(colon + 1).trim();\n if (colon < 0 || !PRESENTATION.has(name) || !validPresentation(name, paint)) return null;\n // Preserve ordered fallback paints, including IcoMo's display-p3 colors.\n entries.push([name, paint]);\n }\n return entries;\n}\n\n/** Parse a bounded static glyph into data, without touching a browser markup sink. */\nexport function parseIconSvgTree(svg: string): IconSvgNode | null {\n if (typeof svg !== 'string' || svg.length > 131072) return null;\n const stack: IconSvgNode[] = [];\n const ids = new Map<string, IconSvgNode>();\n const references = new Map<IconSvgNode, string[]>();\n let root: IconSvgNode | undefined;\n let count = 0;\n function reject(): never {\n throw new Error('Unsupported icon SVG');\n }\n // sax supports strictEntities; its separately published types omit this option.\n const options: sax.SAXOptions & { strictEntities: boolean } = {\n xmlns: true,\n strictEntities: true,\n position: false,\n };\n const xml = sax.parser(true, options);\n // sax reports repeated attributes separately in namespace mode. Do not let\n // its last value silently replace malformed input accepted by the validator.\n const attributeNames = new Set<string>();\n xml.onopentagstart = () => attributeNames.clear();\n xml.onattribute = attribute => {\n if (attributeNames.has(attribute.name)) reject();\n attributeNames.add(attribute.name);\n };\n xml.onerror = reject;\n xml.ondoctype = reject;\n xml.onprocessinginstruction = reject;\n xml.onsgmldeclaration = reject;\n xml.onopencdata = reject;\n xml.onopentag = rawTag => {\n const tag = rawTag as QualifiedTag;\n if (\n ++count > 1024 ||\n stack.length >= 32 ||\n !ELEMENTS.has(tag.name) ||\n tag.prefix ||\n (tag.uri && tag.uri !== SVG_NS)\n )\n reject();\n if (!stack.length && (root || tag.name !== 'svg')) reject();\n const node: IconSvgNode = {\n name: tag.name,\n attributes: Object.create(null),\n styles: [],\n children: [],\n text: '',\n };\n const refs: string[] = [];\n references.set(node, refs);\n for (const { name, value } of Object.values(tag.attributes)) {\n if (!ATTRIBUTES.has(name)) reject();\n const trimmed = value.trim();\n if (name === 'xmlns') {\n if (value !== SVG_NS) reject();\n } else if (name === 'xmlns:xlink') {\n if (value !== XLINK_NS) reject();\n } else if (name === 'id') {\n if (!ID.test(value) || ids.has(value)) reject();\n ids.set(value, node);\n } else if (name === 'style') {\n const styles = parseStyles(value);\n if (!styles) reject();\n node.styles = styles;\n for (const [, paint] of node.styles) {\n const reference = paintReference(paint);\n if (reference) refs.push(reference);\n }\n } else if (name === 'href' || name === 'xlink:href') {\n if (\n !['use', 'linearGradient', 'radialGradient'].includes(tag.name) ||\n !trimmed.startsWith('#') ||\n !ID.test(trimmed.slice(1))\n )\n reject();\n refs.push(trimmed.slice(1));\n } else if (name === 'clip-path' || name === 'mask') {\n if (trimmed !== 'none' && !paintReference(trimmed)) reject();\n const reference = paintReference(trimmed);\n if (reference) refs.push(reference);\n } else if (PRESENTATION.has(name)) {\n if (!validPresentation(name, trimmed)) reject();\n const reference = paintReference(trimmed);\n if (reference) refs.push(reference);\n } else if (!/^[\\w\\s.,%()+-]*$/.test(value)) reject();\n node.attributes[name] = trimmed;\n }\n if (stack.length) stack[stack.length - 1].children.push(node);\n else root = node;\n stack.push(node);\n };\n xml.onclosetag = () => {\n stack.pop();\n };\n xml.ontext = text => {\n const node = stack[stack.length - 1];\n if (node && (node.name === 'title' || node.name === 'desc')) node.text += text;\n else if (text.trim()) reject();\n };\n try {\n xml.write(svg).close();\n if (!root || stack.length) return null;\n // Resolve inside this glyph, never against surrounding page IDs. Reject\n // cycles and bound expansion, including <use> referencing an ancestor.\n let visits = 0;\n const active = new Set<IconSvgNode>();\n const visit = (node: IconSvgNode, depth: number) => {\n if (++visits > 4096 || depth > 64 || active.has(node)) reject();\n active.add(node);\n for (const child of node.children) visit(child, depth + 1);\n for (const id of references.get(node) ?? []) {\n const target = ids.get(id);\n if (!target) reject();\n visit(target, depth + 1);\n }\n active.delete(node);\n };\n visit(root, 0);\n return root;\n } catch {\n return null;\n }\n}\n\nconst SEQUENCE = Symbol.for('ds-mo.icon-svg-sequence');\n\n/** Construct validated SVG with namespaced DOM APIs, without a Trusted Types policy. */\nexport function parseIconSvg(svg: string, ownerDocument?: Document): SVGElement | null {\n const doc = ownerDocument ?? (typeof document === 'undefined' ? undefined : document);\n if (!doc) return null;\n const tree = parseIconSvgTree(svg);\n if (!tree) return null;\n const shared = globalThis as typeof globalThis & { [SEQUENCE]?: number };\n const prefix = 'ds-icon-' + (shared[SEQUENCE] = (shared[SEQUENCE] ?? 0) + 1) + '-';\n const rewrite = (value: string) => {\n const reference = paintReference(value);\n return reference ? 'url(#' + prefix + reference + ')' : value;\n };\n const create = (node: IconSvgNode): SVGElement => {\n const element = doc.createElementNS(SVG_NS, node.name);\n for (const [name, value] of Object.entries(node.attributes)) {\n if (name === 'style' || name.startsWith('xmlns')) continue;\n if (name === 'id') element.id = prefix + value;\n else if (name === 'href' || name === 'xlink:href') {\n element.setAttributeNS(\n name === 'xlink:href' ? XLINK_NS : null,\n name,\n '#' + prefix + value.slice(1)\n );\n } else element.setAttribute(name, rewrite(value));\n }\n for (const [name, value] of node.styles) element.style.setProperty(name, rewrite(value));\n if (node.text) element.appendChild(doc.createTextNode(node.text));\n for (const child of node.children) element.appendChild(create(child));\n return element;\n };\n return create(tree);\n}\n",
|
|
260
261
|
"type": "registry:ui"
|
|
261
262
|
},
|
|
262
263
|
{
|
|
@@ -556,7 +556,7 @@
|
|
|
556
556
|
},
|
|
557
557
|
{
|
|
558
558
|
"path": "src/wc/components/InputDate/InputDate.tsx",
|
|
559
|
-
"content": "import {\n AttachInternals,\n Component,\n Element,\n Prop,\n State,\n Event,\n EventEmitter,\n Listen,\n Method,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n formatIsoCalendarDateLabel,\n isIsoCalendarDate,\n parseLooseCalendarDate,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n restoreStringFormState,\n setFormControlValue,\n TOKEN_DEFAULTS,\n type ControlSize,\n type ControlWidth,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\n\nexport type InputDateSize = ControlSize;\nexport type InputDateWidth = ControlWidth;\n\nlet idCounter = 0;\n\nconst INVALID_DATE_MESSAGE = 'Enter a valid date.';\nconst DATE_BEFORE_MIN_MESSAGE = 'Date is before the minimum allowed date.';\nconst DATE_AFTER_MAX_MESSAGE = 'Date is after the maximum allowed date.';\n\n@Component({\n tag: 'ds-input-date',\n styleUrl: 'InputDate.css',\n scoped: true,\n formAssociated: true,\n})\nexport class InputDate {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-date-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n private popupId = `${this.generatedId}-popup`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() min: string | undefined;\n @Prop() max: string | undefined;\n @Prop() size: InputDateSize = 'md';\n @Prop() width: InputDateWidth = 'fill';\n @Prop() hasBorder: boolean = true;\n @Prop() hasInteractionFill: boolean = true;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n\n private initialValue = '';\n private inputEl?: HTMLInputElement;\n private controlEl?: HTMLElement;\n private calendarButton?: HTMLDsButtonUnfilledElement;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private draftText = '';\n @State() private open = false;\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos = { x: 0, y: 0 };\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.controlEl ?? null,\n getPopup: () => this.el.querySelector<HTMLElement>('.input-date-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect: anchor.getBoundingClientRect(),\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: 'bottom',\n align: 'end',\n sideOffsetPx: resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'end',\n alignOffsetPx: 0,\n sectionInsetPx,\n anchorAlignment: 'popup-frame',\n }),\n viewportPadPx: resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050),\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.controlEl ?? null,\n getPopup: () => this.el.querySelector<HTMLElement>('.input-date-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.closePicker(false),\n });\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get formInactive(): boolean {\n return this.isInactive || this.disabled || this.formDisabled;\n }\n\n private get pickerInactive(): boolean {\n return this.formInactive || this.readOnly;\n }\n\n private get hasConstraintError(): boolean {\n if (!this.value) return false;\n if (!isIsoCalendarDate(this.value)) return true;\n if (this.min && isIsoCalendarDate(this.min) && this.value < this.min) return true;\n if (this.max && isIsoCalendarDate(this.max) && this.value > this.max) return true;\n return false;\n }\n\n private get isInvalid(): boolean {\n return (\n this.error ||\n (!this.formInactive &&\n ((this.required && this.value.length === 0) || this.hasConstraintError))\n );\n }\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n\n disconnectedCallback() {\n this.position.unobserve();\n this.teardownListeners();\n }\n\n @Watch('value')\n @Watch('required')\n @Watch('requiredMessage')\n @Watch('min')\n @Watch('max')\n syncFormValue() {\n const inactive = this.formInactive;\n setFormControlValue(this.internals, this.value, { inactive });\n const flags: ValidityStateFlags = {};\n let message = '';\n if (!inactive) {\n if (this.required && this.value.length === 0) {\n flags.valueMissing = true;\n message = this.requiredMessage;\n } else if (this.value && !isIsoCalendarDate(this.value)) {\n flags.badInput = true;\n message = INVALID_DATE_MESSAGE;\n } else if (this.value && this.min && isIsoCalendarDate(this.min) && this.value < this.min) {\n flags.rangeUnderflow = true;\n message = DATE_BEFORE_MIN_MESSAGE;\n } else if (this.value && this.max && isIsoCalendarDate(this.max) && this.value > this.max) {\n flags.rangeOverflow = true;\n message = DATE_AFTER_MAX_MESSAGE;\n }\n }\n this.internals.setValidity(flags, message);\n }\n\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('readOnly')\n onInteractionStateChange() {\n this.syncFormValue();\n if (this.pickerInactive && this.shouldRender) this.closePicker(false);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.onInteractionStateChange();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n this.draftText = this.displayValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n this.draftText = this.displayValue;\n }\n\n @Method()\n async setFocus() {\n this.inputEl?.focus();\n }\n\n @Listen('keydown')\n handleHostKeyDown(event: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.closePicker('button');\n return;\n }\n if (event.key !== 'Tab') return;\n const popup = this.el.querySelector<HTMLElement>('.input-date-popup');\n if (!popup || !event.composedPath().includes(popup)) return;\n if (!this.interaction.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.closePicker(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get displayValue(): string {\n return formatIsoCalendarDateLabel(this.value);\n }\n\n private parseDraft(text: string): string | null {\n const trimmed = text.trim();\n if (!trimmed) return '';\n const iso = parseLooseCalendarDate(trimmed);\n if (!iso) return null;\n if (this.min && isIsoCalendarDate(this.min) && iso < this.min) return null;\n if (this.max && isIsoCalendarDate(this.max) && iso > this.max) return null;\n return iso;\n }\n\n private commitIso(iso: string, emit = true) {\n if (this.value !== iso) {\n this.value = iso;\n if (emit) this.dsChange.emit(iso);\n }\n this.draftText = formatIsoCalendarDateLabel(iso);\n }\n\n private teardownListeners() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private openPicker() {\n const inactive = this.isInactive || this.disabled || this.formDisabled || this.readOnly;\n if (inactive || this.open) return;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.open = true;\n this.teardownListeners();\n this.interaction.connect();\n this.position.observe();\n this.position.schedule();\n }\n\n private closePicker(restore: 'input' | 'button' | false) {\n if (!this.shouldRender || this.closing) return;\n this.open = false;\n this.position.cancel();\n this.closing = true;\n this.interaction.disconnect();\n this.position.unobserve();\n if (restore === 'input') this.inputEl?.focus();\n if (restore === 'button') void this.calendarButton?.setFocus();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.input-date-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closeTimer = null;\n }\n\n private togglePicker = () => {\n if (this.isInactive || this.disabled || this.formDisabled || this.readOnly) return;\n if (this.open) this.closePicker('button');\n else this.openPicker();\n };\n\n private handleInput = (event: Event) => {\n if (this.pickerInactive) return;\n this.draftText = (event.target as HTMLInputElement).value;\n const parsed = this.parseDraft(this.draftText);\n if (parsed === null) return;\n this.commitIso(parsed);\n };\n\n private handleFocus = () => {\n this.draftText = this.displayValue || this.value;\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n if (this.pickerInactive) {\n this.draftText = this.displayValue;\n return;\n }\n const parsed = this.parseDraft(this.draftText);\n if (parsed === null) {\n this.draftText = this.displayValue;\n return;\n }\n this.commitIso(parsed);\n };\n\n private handleCalendarChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (this.pickerInactive) return;\n const parsed = this.parseDraft(event.detail);\n if (parsed === null) return;\n this.commitIso(parsed);\n this.closePicker('input');\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.formInactive;\n const pickerInactive = this.pickerInactive;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const textClass = `ds-text--${textVariant.replace('text-', '')}`;\n const describedBy =\n [this.ariaDescribedby, showError ? this.errorId : undefined].filter(Boolean).join(' ') ||\n undefined;\n const popupStyle = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-field-stack--supporting-inset': !this.hasBorder,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.isInvalid ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n ref={element => {\n this.controlEl = element;\n }}\n class={{\n 'input-control': true,\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder,\n 'input-control--error': this.hasBorder && this.error,\n 'ds-interaction-fill': this.hasInteractionFill,\n [`ds-control--${this.size}`]: true,\n }}\n >\n <input\n ref={element => {\n this.inputEl = element;\n }}\n type=\"text\"\n id={inputId}\n value={this.focused ? this.draftText : this.displayValue}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete=\"off\"\n spellcheck={false}\n class={{\n 'native-input': true,\n 'native-input--align-start': true,\n 'ds-control-label-box': true,\n [textClass]: true,\n 'ds-text--regular': true,\n }}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.isInvalid ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <ds-button-unfilled\n ref={element => {\n this.calendarButton = element as HTMLDsButtonUnfilledElement | undefined;\n }}\n class=\"input-control__datetime-action\"\n variant=\"icon\"\n size={this.size}\n icon=\"Calendar\"\n hasBorder={false}\n isInset\n isInactive={pickerInactive}\n ariaLabel=\"Choose date\"\n haspopup=\"dialog\"\n expanded={this.open}\n surfaceOpen={this.open || this.closing}\n controls={this.open ? this.popupId : undefined}\n onDsClick={this.togglePicker}\n />\n </div>\n {this.shouldRender ? (\n <div\n id={this.popupId}\n popover=\"manual\"\n class={{\n 'input-date-popup': true,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n }}\n style={popupStyle}\n role=\"dialog\"\n aria-label=\"Choose date\"\n >\n <ds-calendar\n selectionMode=\"single\"\n value={this.value}\n min={this.min}\n max={this.max}\n isInactive={pickerInactive}\n autoFocus\n onDsChange={this.handleCalendarChange}\n />\n </div>\n ) : null}\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
559
|
+
"content": "import {\n AttachInternals,\n Component,\n Element,\n Prop,\n State,\n Event,\n EventEmitter,\n Listen,\n Method,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n formatIsoCalendarDateLabel,\n isIsoCalendarDate,\n parseLooseCalendarDate,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n restoreStringFormState,\n setFormControlValue,\n TOKEN_DEFAULTS,\n type ControlSize,\n type ControlWidth,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\n\nexport type InputDateSize = ControlSize;\nexport type InputDateWidth = ControlWidth;\n\nlet idCounter = 0;\n\nconst INVALID_DATE_MESSAGE = 'Enter a valid date.';\nconst DATE_BEFORE_MIN_MESSAGE = 'Date is before the minimum allowed date.';\nconst DATE_AFTER_MAX_MESSAGE = 'Date is after the maximum allowed date.';\n\n@Component({\n tag: 'ds-input-date',\n styleUrl: 'InputDate.css',\n scoped: true,\n formAssociated: true,\n})\nexport class InputDate {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-date-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n private popupId = `${this.generatedId}-popup`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() min: string | undefined;\n @Prop() max: string | undefined;\n @Prop() size: InputDateSize = 'md';\n @Prop() width: InputDateWidth = 'fill';\n @Prop() hasBorder: boolean = true;\n @Prop() hasInteractionFill: boolean = true;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n\n private initialValue = '';\n private inputEl?: HTMLInputElement;\n private controlEl?: HTMLElement;\n private calendarButton?: HTMLDsButtonUnfilledElement;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private draftText = '';\n @State() private open = false;\n @State() private shouldRender = false;\n @State() private closing = false;\n @State() private positionReady = false;\n @State() private pos = { x: 0, y: 0 };\n\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.controlEl ?? null,\n getPopup: () => this.el.querySelector<HTMLElement>('.input-date-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.open) return null;\n const sectionInsetPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n return {\n anchorRect: anchor.getBoundingClientRect(),\n popupWidth: popup.offsetWidth || this.popupFallbackWidth,\n popupHeight: popup.offsetHeight || this.popupFallbackHeight,\n side: 'bottom',\n align: 'end',\n sideOffsetPx: resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050),\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'end',\n alignOffsetPx: 0,\n sectionInsetPx,\n anchorAlignment: 'popup-frame',\n }),\n viewportPadPx: resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050),\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n observeResize: true,\n topLayer: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.controlEl ?? null,\n getPopup: () => this.el.querySelector<HTMLElement>('.input-date-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.closePicker(false),\n });\n\n private get popupFallbackWidth(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeight(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get formInactive(): boolean {\n return this.isInactive || this.disabled || this.formDisabled;\n }\n\n private get pickerInactive(): boolean {\n return this.formInactive || this.readOnly;\n }\n\n private get hasConstraintError(): boolean {\n if (!this.value) return false;\n if (!isIsoCalendarDate(this.value)) return true;\n if (this.min && isIsoCalendarDate(this.min) && this.value < this.min) return true;\n if (this.max && isIsoCalendarDate(this.max) && this.value > this.max) return true;\n return false;\n }\n\n private get isInvalid(): boolean {\n return (\n this.error ||\n (!this.formInactive &&\n ((this.required && this.value.length === 0) || this.hasConstraintError))\n );\n }\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n\n disconnectedCallback() {\n this.teardownListeners();\n this.open = false;\n this.shouldRender = false;\n this.closing = false;\n this.positionReady = false;\n this.focused = false;\n }\n\n @Watch('value')\n @Watch('required')\n @Watch('requiredMessage')\n @Watch('min')\n @Watch('max')\n syncFormValue() {\n const inactive = this.formInactive;\n setFormControlValue(this.internals, this.value, { inactive });\n const flags: ValidityStateFlags = {};\n let message = '';\n if (!inactive) {\n if (this.required && this.value.length === 0) {\n flags.valueMissing = true;\n message = this.requiredMessage;\n } else if (this.value && !isIsoCalendarDate(this.value)) {\n flags.badInput = true;\n message = INVALID_DATE_MESSAGE;\n } else if (this.value && this.min && isIsoCalendarDate(this.min) && this.value < this.min) {\n flags.rangeUnderflow = true;\n message = DATE_BEFORE_MIN_MESSAGE;\n } else if (this.value && this.max && isIsoCalendarDate(this.max) && this.value > this.max) {\n flags.rangeOverflow = true;\n message = DATE_AFTER_MAX_MESSAGE;\n }\n }\n this.internals.setValidity(flags, message);\n }\n\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('readOnly')\n onInteractionStateChange() {\n this.syncFormValue();\n if (this.pickerInactive && this.shouldRender) this.closePicker(false);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.onInteractionStateChange();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n this.draftText = this.displayValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n this.draftText = this.displayValue;\n }\n\n @Method()\n async setFocus() {\n this.inputEl?.focus();\n }\n\n @Listen('keydown')\n handleHostKeyDown(event: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.closePicker('button');\n return;\n }\n if (event.key !== 'Tab') return;\n const popup = this.el.querySelector<HTMLElement>('.input-date-popup');\n if (!popup || !event.composedPath().includes(popup)) return;\n if (!this.interaction.tabLeavesPopup(event)) return;\n event.preventDefault();\n this.closePicker(false);\n this.interaction.moveFocusAfterTab(event.shiftKey);\n }\n\n private get displayValue(): string {\n return formatIsoCalendarDateLabel(this.value);\n }\n\n private parseDraft(text: string): string | null {\n const trimmed = text.trim();\n if (!trimmed) return '';\n const iso = parseLooseCalendarDate(trimmed);\n if (!iso) return null;\n if (this.min && isIsoCalendarDate(this.min) && iso < this.min) return null;\n if (this.max && isIsoCalendarDate(this.max) && iso > this.max) return null;\n return iso;\n }\n\n private commitIso(iso: string, emit = true) {\n if (this.value !== iso) {\n this.value = iso;\n if (emit) this.dsChange.emit(iso);\n }\n this.draftText = formatIsoCalendarDateLabel(iso);\n }\n\n private teardownListeners() {\n this.position.unobserve();\n this.interaction.disconnect();\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private openPicker() {\n const inactive = this.isInactive || this.disabled || this.formDisabled || this.readOnly;\n if (inactive || this.open || !this.el.isConnected) return;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.open = true;\n this.teardownListeners();\n this.interaction.connect();\n this.position.observe();\n this.position.schedule();\n }\n\n private closePicker(restore: 'input' | 'button' | false) {\n if (!this.shouldRender || this.closing) return;\n this.open = false;\n this.position.cancel();\n this.closing = true;\n this.interaction.disconnect();\n this.position.unobserve();\n if (restore === 'input') this.inputEl?.focus();\n if (restore === 'button') void this.calendarButton?.setFocus();\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort2,\n TOKEN_DEFAULTS.animationDurationShort3\n );\n if (duration <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => this.finishClose(), duration);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.input-date-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.closeTimer = null;\n }\n\n private togglePicker = () => {\n if (this.isInactive || this.disabled || this.formDisabled || this.readOnly) return;\n if (this.open) this.closePicker('button');\n else this.openPicker();\n };\n\n private handleInput = (event: Event) => {\n if (this.pickerInactive) return;\n this.draftText = (event.target as HTMLInputElement).value;\n const parsed = this.parseDraft(this.draftText);\n if (parsed === null) return;\n this.commitIso(parsed);\n };\n\n private handleFocus = () => {\n this.draftText = this.displayValue || this.value;\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n if (this.pickerInactive) {\n this.draftText = this.displayValue;\n return;\n }\n const parsed = this.parseDraft(this.draftText);\n if (parsed === null) {\n this.draftText = this.displayValue;\n return;\n }\n this.commitIso(parsed);\n };\n\n private handleCalendarChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (this.pickerInactive) return;\n const parsed = this.parseDraft(event.detail);\n if (parsed === null) return;\n this.commitIso(parsed);\n this.closePicker('input');\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.formInactive;\n const pickerInactive = this.pickerInactive;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const textClass = `ds-text--${textVariant.replace('text-', '')}`;\n const describedBy =\n [this.ariaDescribedby, showError ? this.errorId : undefined].filter(Boolean).join(' ') ||\n undefined;\n const popupStyle = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-field-stack--supporting-inset': !this.hasBorder,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.isInvalid ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n ref={element => {\n this.controlEl = element;\n }}\n class={{\n 'input-control': true,\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder,\n 'input-control--error': this.hasBorder && this.error,\n 'ds-interaction-fill': this.hasInteractionFill,\n [`ds-control--${this.size}`]: true,\n }}\n >\n <input\n ref={element => {\n this.inputEl = element;\n }}\n type=\"text\"\n id={inputId}\n value={this.focused ? this.draftText : this.displayValue}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete=\"off\"\n spellcheck={false}\n class={{\n 'native-input': true,\n 'native-input--align-start': true,\n 'ds-control-label-box': true,\n [textClass]: true,\n 'ds-text--regular': true,\n }}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.isInvalid ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <ds-button-unfilled\n ref={element => {\n this.calendarButton = element as HTMLDsButtonUnfilledElement | undefined;\n }}\n class=\"input-control__datetime-action\"\n variant=\"icon\"\n size={this.size}\n icon=\"Calendar\"\n hasBorder={false}\n isInset\n isInactive={pickerInactive}\n ariaLabel=\"Choose date\"\n haspopup=\"dialog\"\n expanded={this.open}\n surfaceOpen={this.open || this.closing}\n controls={this.open ? this.popupId : undefined}\n onDsClick={this.togglePicker}\n />\n </div>\n {this.shouldRender ? (\n <div\n id={this.popupId}\n popover=\"manual\"\n class={{\n 'input-date-popup': true,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n }}\n style={popupStyle}\n role=\"dialog\"\n aria-label=\"Choose date\"\n >\n <ds-calendar\n selectionMode=\"single\"\n value={this.value}\n min={this.min}\n max={this.max}\n isInactive={pickerInactive}\n autoFocus\n onDsChange={this.handleCalendarChange}\n />\n </div>\n ) : null}\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
560
560
|
"type": "registry:ui"
|
|
561
561
|
}
|
|
562
562
|
]
|