@ds-mo/ui 15.11.2 → 15.12.1
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/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +120 -19
- package/dist/angular/ds-card-action-center.d.ts +1 -1
- package/dist/angular/ds-card-action-center.js +3 -3
- package/dist/angular/ds-card-chart.d.ts +1 -1
- package/dist/angular/ds-card-chart.js +3 -3
- package/dist/angular/ds-card-navigation.d.ts +1 -1
- package/dist/angular/ds-card-navigation.js +3 -3
- package/dist/angular/ds-card-setting.d.ts +1 -1
- package/dist/angular/ds-card-setting.js +3 -3
- package/dist/angular/ds-card-settings-scope.d.ts +1 -1
- package/dist/angular/ds-card-settings-scope.js +3 -3
- package/dist/angular/ds-field.d.ts +1 -1
- package/dist/angular/ds-field.js +3 -3
- package/dist/angular/ds-inline-banner-settings.d.ts +1 -1
- package/dist/angular/ds-inline-banner-settings.js +3 -3
- package/dist/angular/ds-pagination.d.ts +1 -1
- package/dist/angular/ds-pagination.js +3 -3
- package/dist/angular/ds-radio.d.ts +1 -1
- package/dist/angular/ds-radio.js +3 -3
- package/dist/angular/ds-setting-row-radio.d.ts +1 -1
- package/dist/angular/ds-setting-row-radio.js +3 -3
- package/dist/angular/ds-setting-row-toggle.d.ts +1 -1
- package/dist/angular/ds-setting-row-toggle.js +3 -3
- package/dist/angular/ds-skeleton.d.ts +1 -1
- package/dist/angular/ds-skeleton.js +3 -3
- 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-response.js.map +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-badge.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-action.js +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-action-center.js.map +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-chart.js.map +1 -1
- package/dist/components/ds-card-navigation.js +1 -1
- package/dist/components/ds-card-navigation.js.map +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-card-setting.js.map +1 -1
- package/dist/components/ds-card-settings-scope.js +1 -1
- package/dist/components/ds-card-settings-scope.js.map +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart.js +1 -1
- package/dist/components/ds-checkbox-group.js +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-item.js +1 -1
- package/dist/components/ds-conversation-list-section.js +1 -1
- package/dist/components/ds-conversation-list.js +1 -1
- package/dist/components/ds-data-customize.js +1 -1
- package/dist/components/ds-data-customize.js.map +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-saved-views.js +1 -1
- package/dist/components/ds-data-saved-views.js.map +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-field.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-inline-banner-settings.js +1 -1
- package/dist/components/ds-inline-banner-settings.js.map +1 -1
- package/dist/components/ds-input-date.js +1 -1
- package/dist/components/ds-input-time.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-map-cluster-marker.js +1 -1
- package/dist/components/ds-map-entity-marker.js +1 -1
- package/dist/components/ds-map-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-navigation-tab-group.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-paper-texture.js +1 -1
- package/dist/components/ds-radio-tile.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-score.js +1 -1
- package/dist/components/ds-scroll-overlay.js +1 -1
- package/dist/components/ds-select-toggle.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-setting-row-checkbox.js +1 -1
- package/dist/components/ds-setting-row-radio.js +1 -1
- package/dist/components/ds-setting-row-radio.js.map +1 -1
- package/dist/components/ds-setting-row-toggle.js +1 -1
- package/dist/components/ds-setting-row-toggle.js.map +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-skeleton.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-swatch-picker.js +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-text.js +1 -1
- package/dist/components/ds-textarea.js +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/ds-typing-indicator.js +1 -1
- package/dist/components/p-3Xp9hXbL.js +2 -0
- package/dist/components/{p-BI6L3JoU.js.map → p-3Xp9hXbL.js.map} +1 -1
- package/dist/components/{p-6QYDIPd1.js → p-3dG9v5Aw.js} +2 -2
- package/dist/components/{p-C4kVTIBc.js → p-5PReYMiY.js} +2 -2
- package/dist/components/{p-CzTJAO2J.js → p-8fddX3V3.js} +2 -2
- package/dist/components/{p-Cw1ljgW1.js → p-9u2WDF0J.js} +2 -2
- package/dist/components/{p-M9pOLcyK.js → p-B-ChwMFE.js} +2 -2
- package/dist/components/{p-BvlkO__o.js → p-BAIYOobl.js} +2 -2
- package/dist/components/{p-BrJebF5t.js → p-BIMW5_Z7.js} +2 -2
- package/dist/components/{p-BkKdzYZP.js → p-BMI0F08u.js} +2 -2
- package/dist/components/{p-D0kzIzGQ.js → p-BV8sNEVP.js} +2 -2
- package/dist/components/{p-DNtwJJOC.js → p-BVUdtuog.js} +2 -2
- package/dist/components/{p-CgYDQSLW.js → p-BYo1-6b5.js} +2 -2
- package/dist/components/{p-CJ-igzsZ.js → p-BZbJkJpG.js} +2 -2
- package/dist/components/{p-D2rASHHq.js → p-BcYzHn1E.js} +2 -2
- package/dist/components/{p-0EpjqRWc.js → p-BfRAVR40.js} +2 -2
- package/dist/components/p-BhbniDw4.js +2 -0
- package/dist/components/p-BhbniDw4.js.map +1 -0
- package/dist/components/{p-BBZz2Dvg.js → p-BoWpIl_m.js} +2 -2
- package/dist/components/{p-529k8euH.js → p-BpPHCF63.js} +2 -2
- package/dist/components/{p-DXEInGEq.js → p-By_jYvHp.js} +2 -2
- package/dist/components/p-C5HGwzy9.js +2 -0
- package/dist/components/{p-CXF8wj9I.js.map → p-C5HGwzy9.js.map} +1 -1
- package/dist/components/{p-D4r6oWyQ.js → p-C860ilDn.js} +2 -2
- package/dist/components/{p-DcVK0W4X.js → p-CEHhx4VQ.js} +2 -2
- package/dist/components/p-CGtq7TnC.js +2 -0
- package/dist/components/p-CGtq7TnC.js.map +1 -0
- package/dist/components/p-CL0FpMKo.js +2 -0
- package/dist/components/{p-BjYoM1f8.js.map → p-CL0FpMKo.js.map} +1 -1
- package/dist/components/{p-CEm0AoDR.js → p-CQnjpuMX.js} +2 -2
- package/dist/components/{p-4qKHg502.js → p-CUG4EkKn.js} +2 -2
- package/dist/components/{p-CTstnsLe.js → p-CYmz92EF.js} +2 -2
- package/dist/components/{p-YE1r-XSR.js → p-CbP9ASmI.js} +2 -2
- package/dist/components/{p-C31AjGbg.js → p-CbPGO7vl.js} +2 -2
- package/dist/components/{p-DfphSI9S.js → p-CdMIA5hz.js} +2 -2
- package/dist/components/{p-Ds58qo_J.js → p-CeaIWw1Q.js} +2 -2
- package/dist/components/{p-DxVokKm8.js → p-Cfqvxxs2.js} +2 -2
- package/dist/components/{p-CsPqGA8B.js → p-CpqLGzv4.js} +2 -2
- package/dist/components/p-D538svhW.js +2 -0
- package/dist/components/p-D538svhW.js.map +1 -0
- package/dist/components/{p-DKg181YU.js → p-DFR9hqDU.js} +2 -2
- package/dist/components/{p-DEJLfD7k.js → p-DKZP3jCY.js} +2 -2
- package/dist/components/{p-CTAF7SkL.js → p-DMPn_dt7.js} +2 -2
- package/dist/components/{p-CYRTkP1A.js → p-DYruK9cb.js} +2 -2
- package/dist/components/p-DcvPY30Q.js +2 -0
- package/dist/components/p-DcvPY30Q.js.map +1 -0
- package/dist/components/{p-CqfYCn-e.js → p-DiUaIJ91.js} +2 -2
- package/dist/components/{p-Cqg9T781.js → p-DimFbKZH.js} +2 -2
- package/dist/components/{p-DHmHllUM.js → p-DuhJ49iN.js} +2 -2
- package/dist/components/{p-D_iZPukF.js → p-G6r6yYg8.js} +2 -2
- package/dist/components/{p-87FjWil8.js → p-IQdLCfJJ.js} +2 -2
- package/dist/components/{p-CAARHqjE.js → p-KTaiJztz.js} +2 -2
- package/dist/components/{p-BM-UYlIS.js → p-N-2CZze7.js} +2 -2
- package/dist/components/{p-Ch00yccW.js → p-Nx52go85.js} +2 -2
- package/dist/components/{p-DDawngok.js → p-P2-eHQJU.js} +2 -2
- package/dist/components/{p-CC4X8rpT.js → p-WEffoQQD.js} +2 -2
- package/dist/components/{p-vrRbdZAI.js → p-nakO4viw.js} +2 -2
- package/dist/components/{p-BC9xYdJD.js → p-pkJ3X3s7.js} +2 -2
- package/dist/components/{p-C6VWVB87.js → p-r_e6_0nP.js} +2 -2
- package/dist/docs/components.json +656 -7
- package/dist/lint/index.js +1 -1
- package/dist/mcp-data/registry/card-action-center.json +23 -3
- package/dist/mcp-data/registry/card-chart.json +23 -3
- package/dist/mcp-data/registry/card-navigation.json +23 -3
- package/dist/mcp-data/registry/card-overview.json +2 -2
- package/dist/mcp-data/registry/card-setting.json +23 -3
- package/dist/mcp-data/registry/card-settings-scope.json +22 -2
- package/dist/mcp-data/registry/field.json +22 -2
- package/dist/mcp-data/registry/inline-banner-settings.json +22 -2
- package/dist/mcp-data/registry/pagination.json +22 -2
- package/dist/mcp-data/registry/radio.json +22 -2
- package/dist/mcp-data/registry/registry.json +119 -18
- package/dist/mcp-data/registry/setting-row-radio.json +22 -2
- package/dist/mcp-data/registry/setting-row-toggle.json +23 -3
- package/dist/mcp-data/registry/shell-app.json +3 -2
- package/dist/mcp-data/registry/skeleton.json +120 -9
- package/dist/mcp-data/registry/table.json +7 -6
- package/dist/types/components/CardActionCenter/CardActionCenter.d.ts +2 -0
- package/dist/types/components/CardChart/CardChart.d.ts +2 -0
- package/dist/types/components/CardNavigation/CardNavigation.d.ts +2 -0
- package/dist/types/components/CardSetting/CardSetting.d.ts +2 -0
- package/dist/types/components/CardSettingsScope/CardSettingsScope.d.ts +2 -0
- package/dist/types/components/Field/Field.d.ts +3 -0
- package/dist/types/components/InlineBannerSettings/InlineBannerSettings.d.ts +2 -0
- package/dist/types/components/Pagination/Pagination.d.ts +3 -0
- package/dist/types/components/Radio/Radio.d.ts +2 -0
- package/dist/types/components/SettingRowRadio/SettingRowRadio.d.ts +2 -0
- package/dist/types/components/SettingRowToggle/SettingRowToggle.d.ts +2 -0
- package/dist/types/components/Skeleton/Skeleton.d.ts +24 -1
- package/dist/types/components/Skeleton/index.d.ts +1 -1
- package/dist/types/components/Table/Table.d.ts +1 -1
- package/dist/types/components.d.ts +180 -4
- package/dist/vue/ds-card-action-center.js +1 -0
- package/dist/vue/ds-card-chart.js +1 -0
- package/dist/vue/ds-card-navigation.js +1 -0
- package/dist/vue/ds-card-setting.js +1 -0
- package/dist/vue/ds-card-settings-scope.js +1 -0
- package/dist/vue/ds-field.js +2 -1
- package/dist/vue/ds-inline-banner-settings.js +2 -1
- package/dist/vue/ds-pagination.js +1 -0
- package/dist/vue/ds-radio.js +1 -0
- package/dist/vue/ds-setting-row-radio.js +2 -1
- package/dist/vue/ds-setting-row-toggle.js +1 -0
- package/dist/vue/ds-skeleton.js +5 -0
- package/package.json +1 -1
- package/dist/components/p-BI6L3JoU.js +0 -2
- package/dist/components/p-BjYoM1f8.js +0 -2
- package/dist/components/p-CXF8wj9I.js +0 -2
- package/dist/components/p-Cnzry0_v.js +0 -2
- package/dist/components/p-Cnzry0_v.js.map +0 -1
- package/dist/components/p-DwcAROOo.js +0 -2
- package/dist/components/p-DwcAROOo.js.map +0 -1
- package/dist/components/p-hWKa8Jn8.js +0 -2
- package/dist/components/p-hWKa8Jn8.js.map +0 -1
- package/dist/components/p-xCqqM1Rg.js +0 -2
- package/dist/components/p-xCqqM1Rg.js.map +0 -1
- /package/dist/components/{p-6QYDIPd1.js.map → p-3dG9v5Aw.js.map} +0 -0
- /package/dist/components/{p-C4kVTIBc.js.map → p-5PReYMiY.js.map} +0 -0
- /package/dist/components/{p-CzTJAO2J.js.map → p-8fddX3V3.js.map} +0 -0
- /package/dist/components/{p-Cw1ljgW1.js.map → p-9u2WDF0J.js.map} +0 -0
- /package/dist/components/{p-M9pOLcyK.js.map → p-B-ChwMFE.js.map} +0 -0
- /package/dist/components/{p-BvlkO__o.js.map → p-BAIYOobl.js.map} +0 -0
- /package/dist/components/{p-BrJebF5t.js.map → p-BIMW5_Z7.js.map} +0 -0
- /package/dist/components/{p-BkKdzYZP.js.map → p-BMI0F08u.js.map} +0 -0
- /package/dist/components/{p-D0kzIzGQ.js.map → p-BV8sNEVP.js.map} +0 -0
- /package/dist/components/{p-DNtwJJOC.js.map → p-BVUdtuog.js.map} +0 -0
- /package/dist/components/{p-CgYDQSLW.js.map → p-BYo1-6b5.js.map} +0 -0
- /package/dist/components/{p-CJ-igzsZ.js.map → p-BZbJkJpG.js.map} +0 -0
- /package/dist/components/{p-D2rASHHq.js.map → p-BcYzHn1E.js.map} +0 -0
- /package/dist/components/{p-0EpjqRWc.js.map → p-BfRAVR40.js.map} +0 -0
- /package/dist/components/{p-BBZz2Dvg.js.map → p-BoWpIl_m.js.map} +0 -0
- /package/dist/components/{p-529k8euH.js.map → p-BpPHCF63.js.map} +0 -0
- /package/dist/components/{p-DXEInGEq.js.map → p-By_jYvHp.js.map} +0 -0
- /package/dist/components/{p-D4r6oWyQ.js.map → p-C860ilDn.js.map} +0 -0
- /package/dist/components/{p-DcVK0W4X.js.map → p-CEHhx4VQ.js.map} +0 -0
- /package/dist/components/{p-CEm0AoDR.js.map → p-CQnjpuMX.js.map} +0 -0
- /package/dist/components/{p-4qKHg502.js.map → p-CUG4EkKn.js.map} +0 -0
- /package/dist/components/{p-CTstnsLe.js.map → p-CYmz92EF.js.map} +0 -0
- /package/dist/components/{p-YE1r-XSR.js.map → p-CbP9ASmI.js.map} +0 -0
- /package/dist/components/{p-C31AjGbg.js.map → p-CbPGO7vl.js.map} +0 -0
- /package/dist/components/{p-DfphSI9S.js.map → p-CdMIA5hz.js.map} +0 -0
- /package/dist/components/{p-Ds58qo_J.js.map → p-CeaIWw1Q.js.map} +0 -0
- /package/dist/components/{p-DxVokKm8.js.map → p-Cfqvxxs2.js.map} +0 -0
- /package/dist/components/{p-CsPqGA8B.js.map → p-CpqLGzv4.js.map} +0 -0
- /package/dist/components/{p-DKg181YU.js.map → p-DFR9hqDU.js.map} +0 -0
- /package/dist/components/{p-DEJLfD7k.js.map → p-DKZP3jCY.js.map} +0 -0
- /package/dist/components/{p-CTAF7SkL.js.map → p-DMPn_dt7.js.map} +0 -0
- /package/dist/components/{p-CYRTkP1A.js.map → p-DYruK9cb.js.map} +0 -0
- /package/dist/components/{p-CqfYCn-e.js.map → p-DiUaIJ91.js.map} +0 -0
- /package/dist/components/{p-Cqg9T781.js.map → p-DimFbKZH.js.map} +0 -0
- /package/dist/components/{p-DHmHllUM.js.map → p-DuhJ49iN.js.map} +0 -0
- /package/dist/components/{p-D_iZPukF.js.map → p-G6r6yYg8.js.map} +0 -0
- /package/dist/components/{p-87FjWil8.js.map → p-IQdLCfJJ.js.map} +0 -0
- /package/dist/components/{p-CAARHqjE.js.map → p-KTaiJztz.js.map} +0 -0
- /package/dist/components/{p-BM-UYlIS.js.map → p-N-2CZze7.js.map} +0 -0
- /package/dist/components/{p-Ch00yccW.js.map → p-Nx52go85.js.map} +0 -0
- /package/dist/components/{p-DDawngok.js.map → p-P2-eHQJU.js.map} +0 -0
- /package/dist/components/{p-CC4X8rpT.js.map → p-WEffoQQD.js.map} +0 -0
- /package/dist/components/{p-vrRbdZAI.js.map → p-nakO4viw.js.map} +0 -0
- /package/dist/components/{p-BC9xYdJD.js.map → p-pkJ3X3s7.js.map} +0 -0
- /package/dist/components/{p-C6VWVB87.js.map → p-r_e6_0nP.js.map} +0 -0
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"The shared text-action recipe supplies brand foreground, hover underline, and keyboard focus indication."
|
|
33
33
|
],
|
|
34
34
|
"states": [
|
|
35
|
+
"Loading retains the card surface, sentence wrapping, and control footprints while replacing each fragment with an inert text skeleton. Text actions mask only their labels, preserving the surrounding button's hit area.",
|
|
35
36
|
"The application owns the selected area, profile, available choices, permissions, and empty-state copy. Existing complete area-trigger labels remain compatible. Applications that separate the localizable settings noun from the control supply the area-only trigger label and noun together.",
|
|
36
37
|
"The card reports popup requests without changing selection or expanded state.",
|
|
37
38
|
"The surface follows the active theme rather than imposing always-dark colors."
|
|
@@ -112,6 +113,15 @@
|
|
|
112
113
|
"mutable": false,
|
|
113
114
|
"description": "Sentence connector, supplied separately for localization."
|
|
114
115
|
},
|
|
116
|
+
"isLoading": {
|
|
117
|
+
"type": "boolean",
|
|
118
|
+
"resolvedType": "boolean",
|
|
119
|
+
"attribute": "is-loading",
|
|
120
|
+
"default": "false",
|
|
121
|
+
"required": false,
|
|
122
|
+
"mutable": false,
|
|
123
|
+
"description": "Replace sentence fragments and controls with skeletons inside the same card."
|
|
124
|
+
},
|
|
115
125
|
"managingLabel": {
|
|
116
126
|
"type": "string",
|
|
117
127
|
"resolvedType": "string",
|
|
@@ -224,6 +234,15 @@
|
|
|
224
234
|
"mutable": false,
|
|
225
235
|
"description": "Sentence connector, supplied separately for localization."
|
|
226
236
|
},
|
|
237
|
+
"isLoading": {
|
|
238
|
+
"type": "boolean",
|
|
239
|
+
"resolvedType": "boolean",
|
|
240
|
+
"attribute": "is-loading",
|
|
241
|
+
"default": "false",
|
|
242
|
+
"required": false,
|
|
243
|
+
"mutable": false,
|
|
244
|
+
"description": "Replace sentence fragments and controls with skeletons inside the same card."
|
|
245
|
+
},
|
|
227
246
|
"managingLabel": {
|
|
228
247
|
"type": "string",
|
|
229
248
|
"resolvedType": "string",
|
|
@@ -338,17 +357,18 @@
|
|
|
338
357
|
"@ds-mo/tokens"
|
|
339
358
|
],
|
|
340
359
|
"registryDependencies": [
|
|
360
|
+
"skeleton",
|
|
341
361
|
"text"
|
|
342
362
|
],
|
|
343
363
|
"files": [
|
|
344
364
|
{
|
|
345
365
|
"path": "src/wc/components/CardSettingsScope/CardSettingsScope.css",
|
|
346
|
-
"content": "@import '../../utils/text-decoration.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/radius.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n min-width: 0;\n width: var(--dimension-card-width-md);\n max-width: 100%;\n}\n\n.card-settings-scope {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 0 var(--dimension-space-050);\n box-sizing: border-box;\n min-height: var(--dimension-size-600);\n padding: var(--dimension-space-100) var(--dimension-space-200);\n border-radius: var(--ds-radius-card);\n background: var(--color-background-primary);\n color: var(--color-foreground-primary);\n}\n\n.card-settings-scope__action {\n display: inline-flex;\n align-items: center;\n min-height: var(--dimension-size-400);\n max-width: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n cursor: pointer;\n}\n\n.card-settings-scope__action ds-text {\n overflow-wrap: anywhere;\n text-align: start;\n}\n\n@media (forced-colors: active) {\n .card-settings-scope {\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n}\n",
|
|
366
|
+
"content": "@import '../../utils/text-decoration.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/radius.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n min-width: 0;\n width: var(--dimension-card-width-md);\n max-width: 100%;\n}\n\n.card-settings-scope {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 0 var(--dimension-space-050);\n box-sizing: border-box;\n min-height: var(--dimension-size-600);\n padding: var(--dimension-space-100) var(--dimension-space-200);\n border-radius: var(--ds-radius-card);\n background: var(--color-background-primary);\n color: var(--color-foreground-primary);\n}\n\n.card-settings-scope__action {\n display: inline-flex;\n align-items: center;\n min-height: var(--dimension-size-400);\n min-width: 0;\n max-width: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n cursor: pointer;\n}\n\n.card-settings-scope ds-skeleton {\n min-width: 0;\n max-width: 100%;\n}\n\n.card-settings-scope__action ds-text {\n overflow-wrap: anywhere;\n text-align: start;\n}\n\n@media (forced-colors: active) {\n .card-settings-scope {\n outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n outline-offset: calc(-1 * var(--dimension-stroke-width-012));\n }\n}\n",
|
|
347
367
|
"type": "registry:ui"
|
|
348
368
|
},
|
|
349
369
|
{
|
|
350
370
|
"path": "src/wc/components/CardSettingsScope/CardSettingsScope.tsx",
|
|
351
|
-
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\n\nexport interface SettingsScopeRequest {\n scope: 'area' | 'profile';\n anchor: HTMLButtonElement;\n originalEvent: MouseEvent;\n}\n\n@Component({\n tag: 'ds-card-settings-scope',\n styleUrl: 'CardSettingsScope.css',\n scoped: true,\n})\nexport class CardSettingsScope {\n /** Current product area or complete combined-view label in the sentence trigger. */\n @Prop() areaLabel: string = 'All settings';\n /** Current organization or settings profile. */\n @Prop() profileLabel: string = 'Organization';\n /** Sentence prefix, supplied separately for localization. */\n @Prop() managingLabel: string = 'Managing';\n /** Optional sentence noun after an area-only trigger label, supplied separately for localization. */\n @Prop() settingsLabel: string = '';\n /** Sentence connector, supplied separately for localization. */\n @Prop() forLabel: string = 'for';\n /** Accessible name for this context region. */\n @Prop() scopeLabel: string = 'Settings scope';\n /** ID of the application-owned product-area menu. */\n @Prop() areaControls: string | undefined;\n /** ID of the application-owned profiles popup. */\n @Prop() profileControls: string | undefined;\n /** Whether the product-area menu is open. */\n @Prop() areaExpanded: boolean = false;\n /** Whether the profiles popup is open. */\n @Prop() profileExpanded: boolean = false;\n /** Empty informational profile popups use dialog semantics instead of a menu. */\n @Prop() profilePopup: 'menu' | 'dialog' = 'menu';\n\n /** Requests a popup; the owner supplies choices, open state, and selection. */\n @Event() dsScopeRequest!: EventEmitter<SettingsScopeRequest>;\n\n private request(scope: SettingsScopeRequest['scope'], event: MouseEvent) {\n this.dsScopeRequest.emit({\n scope,\n anchor: event.currentTarget as HTMLButtonElement,\n originalEvent: event,\n });\n }\n\n render() {\n return (\n <Host>\n <div\n class=\"card-settings-scope ds-control-elevation ds-control-elevation--sm\"\n role=\"region\"\n aria-label={this.scopeLabel}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"primary\">\n
|
|
371
|
+
"content": "import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\n\nexport interface SettingsScopeRequest {\n scope: 'area' | 'profile';\n anchor: HTMLButtonElement;\n originalEvent: MouseEvent;\n}\n\n@Component({\n tag: 'ds-card-settings-scope',\n styleUrl: 'CardSettingsScope.css',\n scoped: true,\n})\nexport class CardSettingsScope {\n /** Current product area or complete combined-view label in the sentence trigger. */\n @Prop() areaLabel: string = 'All settings';\n /** Current organization or settings profile. */\n @Prop() profileLabel: string = 'Organization';\n /** Sentence prefix, supplied separately for localization. */\n @Prop() managingLabel: string = 'Managing';\n /** Optional sentence noun after an area-only trigger label, supplied separately for localization. */\n @Prop() settingsLabel: string = '';\n /** Sentence connector, supplied separately for localization. */\n @Prop() forLabel: string = 'for';\n /** Accessible name for this context region. */\n @Prop() scopeLabel: string = 'Settings scope';\n /** ID of the application-owned product-area menu. */\n @Prop() areaControls: string | undefined;\n /** ID of the application-owned profiles popup. */\n @Prop() profileControls: string | undefined;\n /** Whether the product-area menu is open. */\n @Prop() areaExpanded: boolean = false;\n /** Whether the profiles popup is open. */\n @Prop() profileExpanded: boolean = false;\n /** Empty informational profile popups use dialog semantics instead of a menu. */\n @Prop() profilePopup: 'menu' | 'dialog' = 'menu';\n /** Replace sentence fragments and controls with skeletons inside the same card. */\n @Prop() isLoading: boolean = false;\n\n /** Requests a popup; the owner supplies choices, open state, and selection. */\n @Event() dsScopeRequest!: EventEmitter<SettingsScopeRequest>;\n\n private request(scope: SettingsScopeRequest['scope'], event: MouseEvent) {\n if (this.isLoading) return;\n this.dsScopeRequest.emit({\n scope,\n anchor: event.currentTarget as HTMLButtonElement,\n originalEvent: event,\n });\n }\n\n render() {\n return (\n <Host aria-busy={this.isLoading ? 'true' : undefined} inert={this.isLoading}>\n <div\n class=\"card-settings-scope ds-control-elevation ds-control-elevation--sm\"\n role=\"region\"\n aria-label={this.scopeLabel}\n aria-hidden={this.isLoading ? 'true' : undefined}\n >\n <ds-skeleton preserveLayout={true} isLoading={this.isLoading}>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"primary\">\n {this.managingLabel}\n </ds-text>\n </ds-skeleton>\n <button\n type=\"button\"\n class=\"card-settings-scope__action ds-text-action ds-focus-ring\"\n aria-haspopup=\"menu\"\n aria-controls={this.areaControls}\n aria-expanded={String(this.areaExpanded)}\n onClick={event => this.request('area', event)}\n >\n <ds-skeleton preserveLayout={true} isLoading={this.isLoading}>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.areaLabel}\n </ds-text>\n </ds-skeleton>\n </button>\n {this.settingsLabel.trim() ? (\n <ds-skeleton preserveLayout={true} isLoading={this.isLoading}>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"primary\">\n {this.settingsLabel}\n </ds-text>\n </ds-skeleton>\n ) : null}\n <ds-skeleton preserveLayout={true} isLoading={this.isLoading}>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"primary\">\n {this.forLabel}\n </ds-text>\n </ds-skeleton>\n <button\n type=\"button\"\n class=\"card-settings-scope__action ds-text-action ds-focus-ring\"\n aria-haspopup={this.profilePopup}\n aria-controls={this.profileControls}\n aria-expanded={String(this.profileExpanded)}\n onClick={event => this.request('profile', event)}\n >\n <ds-skeleton preserveLayout={true} isLoading={this.isLoading}>\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"inherit\">\n {this.profileLabel}\n </ds-text>\n </ds-skeleton>\n </button>\n </div>\n </Host>\n );\n }\n}\n",
|
|
352
372
|
"type": "registry:ui"
|
|
353
373
|
}
|
|
354
374
|
]
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"Do not use placeholder text as the only label. Keep label text concise and put format or consequence guidance in description."
|
|
50
50
|
],
|
|
51
51
|
"states": [
|
|
52
|
+
"Loading masks field copy and the slotted control in place. The control remains mounted with its value, native form association, and accessible label intact; the field is inert until loading ends.",
|
|
52
53
|
"The visible label is optional. Omit it when a nearby heading already names the field; Field still associates description and error text with the slotted control.",
|
|
53
54
|
"Label, control, description, and error content follow the shared 4px vertical field flow without changing the slotted control's width or semantics. When the slotted control is borderless, description and error copy inset to that control's text origin instead of the resting bordered edge.",
|
|
54
55
|
"Field exposes data-focused, data-filled, data-dirty, data-touched, data-invalid, data-disabled, and data-required hooks while the slotted control remains the source of value and interaction behavior.",
|
|
@@ -132,6 +133,15 @@
|
|
|
132
133
|
"mutable": false,
|
|
133
134
|
"description": "Explicit ID for the slotted control; generated when omitted."
|
|
134
135
|
},
|
|
136
|
+
"isLoading": {
|
|
137
|
+
"type": "boolean",
|
|
138
|
+
"resolvedType": "boolean",
|
|
139
|
+
"attribute": "is-loading",
|
|
140
|
+
"default": "false",
|
|
141
|
+
"required": false,
|
|
142
|
+
"mutable": false,
|
|
143
|
+
"description": "Masks field copy and its mounted control while preserving layout and form state."
|
|
144
|
+
},
|
|
135
145
|
"label": {
|
|
136
146
|
"type": "string",
|
|
137
147
|
"resolvedType": "string | undefined",
|
|
@@ -179,6 +189,15 @@
|
|
|
179
189
|
"mutable": false,
|
|
180
190
|
"description": "Explicit ID for the slotted control; generated when omitted."
|
|
181
191
|
},
|
|
192
|
+
"isLoading": {
|
|
193
|
+
"type": "boolean",
|
|
194
|
+
"resolvedType": "boolean",
|
|
195
|
+
"attribute": "is-loading",
|
|
196
|
+
"default": "false",
|
|
197
|
+
"required": false,
|
|
198
|
+
"mutable": false,
|
|
199
|
+
"description": "Masks field copy and its mounted control while preserving layout and form state."
|
|
200
|
+
},
|
|
182
201
|
"label": {
|
|
183
202
|
"type": "string",
|
|
184
203
|
"resolvedType": "string | undefined",
|
|
@@ -230,17 +249,18 @@
|
|
|
230
249
|
"@ds-mo/tokens"
|
|
231
250
|
],
|
|
232
251
|
"registryDependencies": [
|
|
252
|
+
"skeleton",
|
|
233
253
|
"text"
|
|
234
254
|
],
|
|
235
255
|
"files": [
|
|
236
256
|
{
|
|
237
257
|
"path": "src/wc/components/Field/Field.css",
|
|
238
|
-
"content": "@import '../../utils/text-field-support.css';\n@import '../../utils/control-density.css';\n@import '../../utils/field-stack.css';\n\n:host {\n display: block;\n}\n\n.field__control {\n display: flex;\n min-width: 0;\n}\n\n.field__control > * {\n width: 100%;\n}\n",
|
|
258
|
+
"content": "@import '../../utils/text-field-support.css';\n@import '../../utils/control-density.css';\n@import '../../utils/field-stack.css';\n\n:host {\n display: block;\n}\n\n.field__control {\n position: relative;\n display: flex;\n min-width: 0;\n}\n\n.field__control > * {\n width: 100%;\n}\n\n.field__label,\n.field__description,\n.field__error,\n.text-field-count {\n position: relative;\n}\n\n.field__copy-source--loading,\n:host(.field--loading) .field__control ::slotted(:not(.field__control-skeleton)) {\n visibility: hidden;\n}\n\n.field__copy-skeleton {\n position: absolute;\n inset-inline-start: 0;\n inset-block-start: 0;\n max-inline-size: 100%;\n}\n\n.field__label .field__copy-skeleton {\n inline-size: min(100%, calc(var(--dimension-size-800) * 2));\n}\n\n.field__control-skeleton {\n position: absolute;\n inset: 0;\n inline-size: 100%;\n block-size: 100%;\n}\n",
|
|
239
259
|
"type": "registry:ui"
|
|
240
260
|
},
|
|
241
261
|
{
|
|
242
262
|
"path": "src/wc/components/Field/Field.tsx",
|
|
243
|
-
"content": "import { Component, Element, Prop, State, Watch, h, Host } from '@stencil/core';\n\ninterface FieldControl extends HTMLElement {\n value?: string | string[];\n tokens?: string[];\n tokenized?: boolean;\n inputId?: string;\n error?: boolean;\n disabled?: boolean;\n isInactive?: boolean;\n required?: boolean;\n hasBorder?: boolean;\n size?: string;\n setFocus?: () => Promise<void> | void;\n}\n\ninterface AuthoredAria {\n labelledby: string[];\n describedby: string[];\n}\n\nlet fieldCounter = 0;\n\nconst tokens = (value: string | null | undefined): string[] =>\n value?.split(/\\s+/).filter(Boolean) ?? [];\n\nconst uniqueTokens = (...groups: string[][]): string[] => [...new Set(groups.flat())];\n\nconst CONTROL_SIZES = ['lg', 'md', 'sm', 'xs'] as const;\ntype FieldControlSize = (typeof CONTROL_SIZES)[number];\n\nconst readControlSize = (control: FieldControl): FieldControlSize =>\n CONTROL_SIZES.includes(control.size as FieldControlSize)\n ? (control.size as FieldControlSize)\n : 'md';\n\n@Component({\n tag: 'ds-field',\n styleUrl: 'Field.css',\n scoped: true,\n})\nexport class Field {\n @Element() el!: HTMLElement;\n\n /** Visible label for the slotted control. Omit when a nearby heading already names the field. */\n @Prop() label?: string;\n /** Explicit ID for the slotted control; generated when omitted. */\n @Prop() fieldId: string | undefined;\n /** Optional guidance associated with the slotted control while no visible error is shown. */\n @Prop() description: string | undefined;\n /** Invalid visual and accessible state forwarded to supported ds controls. */\n @Prop() error: boolean = false;\n /** Visible error associated with the slotted control while error is true. */\n @Prop() errorMessage: string | undefined;\n\n @State() private constraintMessage = '';\n @State() private characterCount: string | undefined;\n @State() private characterLimit: string | undefined;\n @State() private focused = false;\n @State() private filled = false;\n @State() private dirty = false;\n @State() private touched = false;\n @State() private controlDisabled = false;\n @State() private controlRequired = false;\n @State() private controlBorderless = false;\n @State() private controlSize: 'lg' | 'md' | 'sm' | 'xs' = 'md';\n /** Bumped to let Stencil relocate a control added after the initial render. */\n @State() private slotRevision = 0;\n\n private readonly generatedId = `ds-field-${++fieldCounter}`;\n private controlContainer?: HTMLDivElement;\n private control?: FieldControl;\n private initialValue = '';\n private authoredAria = new WeakMap<FieldControl, AuthoredAria>();\n private childObserver?: MutationObserver;\n private controlAttrObserver?: MutationObserver;\n private didLoad = false;\n\n connectedCallback() {\n if (this.didLoad) this.observeLateControls();\n }\n\n disconnectedCallback() {\n this.childObserver?.disconnect();\n this.childObserver = undefined;\n this.controlAttrObserver?.disconnect();\n this.controlAttrObserver = undefined;\n }\n\n componentDidLoad() {\n this.syncControl();\n this.didLoad = true;\n this.observeLateControls();\n requestAnimationFrame(() => this.syncControl());\n }\n\n componentDidRender() {\n this.syncControl();\n }\n\n @Watch('fieldId')\n @Watch('label')\n @Watch('description')\n @Watch('error')\n @Watch('errorMessage')\n onFieldContractChange() {\n this.syncControl();\n }\n\n private get hasLabel(): boolean {\n return Boolean(this.label?.trim());\n }\n\n private get controlId(): string {\n return this.fieldId || this.generatedId;\n }\n\n private get labelId(): string {\n return `${this.controlId}-label`;\n }\n\n private get descriptionId(): string {\n return `${this.controlId}-description`;\n }\n\n private get errorId(): string {\n return `${this.controlId}-error`;\n }\n\n private get renderedDescription(): boolean {\n return Boolean(this.description) && !this.renderedError;\n }\n\n private get renderedError(): boolean {\n return Boolean(this.visibleError);\n }\n\n private get visibleError(): string {\n return this.error && this.errorMessage ? this.errorMessage : this.constraintMessage;\n }\n\n private readValue(control: FieldControl): string {\n if (control.tokenized)\n return [...(control.tokens ?? []), String(control.value ?? '')]\n .filter(Boolean)\n .join('\\u001f');\n const value = control.value;\n return Array.isArray(value) ? value.join('\\u001f') : String(value ?? '');\n }\n\n private updateControlAttribute(control: FieldControl, name: string, value: string | undefined) {\n if (value === undefined) {\n if (control.hasAttribute(name)) control.removeAttribute(name);\n return;\n }\n if (control.getAttribute(name) !== value) control.setAttribute(name, value);\n }\n\n private findControl(): FieldControl | undefined {\n return this.controlContainer?.querySelector<FieldControl>(':scope > :not(slot)') ?? undefined;\n }\n\n private observeLateControls() {\n if (this.childObserver || typeof MutationObserver === 'undefined') return;\n this.childObserver = new MutationObserver(records => {\n const addedControl = records.some(record =>\n Array.from(record.addedNodes).some(node => node.nodeType === 1)\n );\n if (addedControl) this.slotRevision++;\n });\n this.childObserver.observe(this.el, { childList: true });\n }\n\n private syncControl = () => {\n const control = this.findControl();\n if (!control) return;\n\n if (control !== this.control) {\n this.control = control;\n this.initialValue = this.readValue(control);\n this.authoredAria.set(control, {\n labelledby: tokens(control.getAttribute('aria-labelledby')),\n describedby: tokens(control.getAttribute('aria-describedby')),\n });\n this.observeControlAttributes(control);\n }\n\n this.constraintMessage = control.getAttribute('data-constraint-message') ?? '';\n this.characterCount = control.getAttribute('data-character-count') ?? undefined;\n this.characterLimit = control.getAttribute('data-character-limit') ?? undefined;\n\n const isDsControl = control.tagName.startsWith('DS-');\n if (isDsControl && 'inputId' in control) control.inputId = this.controlId;\n else this.updateControlAttribute(control, 'id', this.controlId);\n\n const authored = this.authoredAria.get(control) ?? { labelledby: [], describedby: [] };\n const labelledby = uniqueTokens(authored.labelledby, this.hasLabel ? [this.labelId] : []).join(\n ' '\n );\n const describedby = uniqueTokens(\n authored.describedby,\n this.renderedDescription ? [this.descriptionId] : [],\n this.renderedError ? [this.errorId] : [],\n this.characterCount !== undefined ? [`${this.controlId}-count`] : []\n ).join(' ');\n\n this.updateControlAttribute(control, 'aria-labelledby', labelledby || undefined);\n this.updateControlAttribute(control, 'aria-describedby', describedby || undefined);\n this.updateControlAttribute(control, 'aria-invalid', this.error ? 'true' : undefined);\n\n if (isDsControl && 'error' in control && control.error !== this.error)\n control.error = this.error;\n\n const value = this.readValue(control);\n this.filled = value.length > 0;\n this.dirty = value !== this.initialValue;\n this.controlDisabled = Boolean(\n control.disabled || control.isInactive || control.hasAttribute('disabled')\n );\n this.controlRequired = Boolean(control.required || control.hasAttribute('required'));\n this.controlBorderless = control.hasBorder === false;\n this.controlSize = readControlSize(control);\n };\n\n private observeControlAttributes(control: FieldControl) {\n this.controlAttrObserver?.disconnect();\n this.controlAttrObserver = new MutationObserver(() => this.syncControl());\n this.controlAttrObserver.observe(control, {\n attributes: true,\n attributeFilter: [\n 'has-border',\n 'size',\n 'class',\n 'data-constraint-message',\n 'data-character-count',\n 'data-character-limit',\n ],\n });\n }\n\n private handleLabelClick = (event: MouseEvent) => {\n if (!this.control?.tagName.startsWith('DS-')) return;\n event.preventDefault();\n this.control.setFocus?.();\n };\n\n private handleFocusIn = () => {\n this.focused = true;\n };\n\n private handleFocusOut = (event: FocusEvent) => {\n if (event.relatedTarget instanceof Node && this.el.contains(event.relatedTarget)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleValueChange = () => {\n this.syncControl();\n };\n\n render() {\n return (\n <Host\n data-focused={this.focused ? '' : undefined}\n data-filled={this.filled ? '' : undefined}\n data-dirty={this.dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-invalid={this.error || this.constraintMessage ? '' : undefined}\n data-disabled={this.controlDisabled ? '' : undefined}\n data-required={this.controlRequired ? '' : undefined}\n onFocusin={this.handleFocusIn}\n onFocusout={this.handleFocusOut}\n onInput={this.handleValueChange}\n onChange={this.handleValueChange}\n onDsChange={this.handleValueChange}\n onDsTokensChange={this.handleValueChange}\n >\n <div\n class={{\n field: true,\n 'ds-field-stack': true,\n 'ds-field-stack--supporting-inset': this.controlBorderless,\n [`ds-control--${this.controlSize}`]: true,\n }}\n >\n {this.hasLabel ? (\n <ds-text\n class=\"field__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={this.controlId}\n textId={this.labelId}\n onClick={this.handleLabelClick}\n >\n {this.label}\n </ds-text>\n ) : null}\n <div class=\"field__control\" ref={el => (this.controlContainer = el)}>\n <slot onSlotchange={this.syncControl} />\n </div>\n {(this.renderedDescription ||\n this.renderedError ||\n this.characterCount !== undefined) && (\n <div class=\"text-field-support\">\n {this.renderedDescription && (\n <ds-text\n class=\"field__description\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"secondary\"\n textId={this.descriptionId}\n >\n {this.description}\n </ds-text>\n )}\n {this.renderedError && (\n <ds-text\n class=\"field__error\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.visibleError}\n </ds-text>\n )}\n {this.characterCount !== undefined && (\n <ds-text\n class=\"text-field-count\"\n as=\"span\"\n variant=\"text-body-small\"\n color={this.error || this.constraintMessage ? 'negative' : 'secondary'}\n textId={`${this.controlId}-count`}\n >\n {this.characterCount}/{this.characterLimit}\n </ds-text>\n )}\n </div>\n )}\n </div>\n </Host>\n );\n }\n}\n",
|
|
263
|
+
"content": "import { Component, Element, Prop, State, Watch, h, Host } from '@stencil/core';\n\ninterface FieldControl extends HTMLElement {\n value?: string | string[];\n tokens?: string[];\n tokenized?: boolean;\n inputId?: string;\n error?: boolean;\n disabled?: boolean;\n isInactive?: boolean;\n required?: boolean;\n hasBorder?: boolean;\n size?: string;\n setFocus?: () => Promise<void> | void;\n}\n\ninterface AuthoredAria {\n labelledby: string[];\n describedby: string[];\n}\n\nlet fieldCounter = 0;\n\nconst tokens = (value: string | null | undefined): string[] =>\n value?.split(/\\s+/).filter(Boolean) ?? [];\n\nconst uniqueTokens = (...groups: string[][]): string[] => [...new Set(groups.flat())];\n\nconst CONTROL_SIZES = ['lg', 'md', 'sm', 'xs'] as const;\ntype FieldControlSize = (typeof CONTROL_SIZES)[number];\n\nconst readControlSize = (control: FieldControl): FieldControlSize =>\n CONTROL_SIZES.includes(control.size as FieldControlSize)\n ? (control.size as FieldControlSize)\n : 'md';\n\n@Component({\n tag: 'ds-field',\n styleUrl: 'Field.css',\n scoped: true,\n})\nexport class Field {\n @Element() el!: HTMLElement;\n\n /** Visible label for the slotted control. Omit when a nearby heading already names the field. */\n @Prop() label?: string;\n /** Explicit ID for the slotted control; generated when omitted. */\n @Prop() fieldId: string | undefined;\n /** Optional guidance associated with the slotted control while no visible error is shown. */\n @Prop() description: string | undefined;\n /** Invalid visual and accessible state forwarded to supported ds controls. */\n @Prop() error: boolean = false;\n /** Visible error associated with the slotted control while error is true. */\n @Prop() errorMessage: string | undefined;\n /** Masks field copy and its mounted control while preserving layout and form state. */\n @Prop() isLoading: boolean = false;\n\n @State() private constraintMessage = '';\n @State() private characterCount: string | undefined;\n @State() private characterLimit: string | undefined;\n @State() private focused = false;\n @State() private filled = false;\n @State() private dirty = false;\n @State() private touched = false;\n @State() private controlDisabled = false;\n @State() private controlRequired = false;\n @State() private controlBorderless = false;\n @State() private controlSize: 'lg' | 'md' | 'sm' | 'xs' = 'md';\n /** Bumped to let Stencil relocate a control added after the initial render. */\n @State() private slotRevision = 0;\n\n private readonly generatedId = `ds-field-${++fieldCounter}`;\n private controlContainer?: HTMLDivElement;\n private control?: FieldControl;\n private initialValue = '';\n private authoredAria = new WeakMap<FieldControl, AuthoredAria>();\n private childObserver?: MutationObserver;\n private controlAttrObserver?: MutationObserver;\n private didLoad = false;\n\n connectedCallback() {\n if (this.didLoad) this.observeLateControls();\n }\n\n disconnectedCallback() {\n this.childObserver?.disconnect();\n this.childObserver = undefined;\n this.controlAttrObserver?.disconnect();\n this.controlAttrObserver = undefined;\n }\n\n componentDidLoad() {\n this.syncControl();\n this.didLoad = true;\n this.observeLateControls();\n requestAnimationFrame(() => this.syncControl());\n }\n\n componentDidRender() {\n this.syncControl();\n }\n\n @Watch('fieldId')\n @Watch('label')\n @Watch('description')\n @Watch('error')\n @Watch('errorMessage')\n onFieldContractChange() {\n this.syncControl();\n }\n\n private get hasLabel(): boolean {\n return Boolean(this.label?.trim());\n }\n\n private get controlId(): string {\n return this.fieldId || this.generatedId;\n }\n\n private get labelId(): string {\n return `${this.controlId}-label`;\n }\n\n private get descriptionId(): string {\n return `${this.controlId}-description`;\n }\n\n private get errorId(): string {\n return `${this.controlId}-error`;\n }\n\n private get renderedDescription(): boolean {\n return Boolean(this.description) && !this.renderedError;\n }\n\n private get renderedError(): boolean {\n return Boolean(this.visibleError);\n }\n\n private get visibleError(): string {\n return this.error && this.errorMessage ? this.errorMessage : this.constraintMessage;\n }\n\n private readValue(control: FieldControl): string {\n if (control.tokenized)\n return [...(control.tokens ?? []), String(control.value ?? '')]\n .filter(Boolean)\n .join('\\u001f');\n const value = control.value;\n return Array.isArray(value) ? value.join('\\u001f') : String(value ?? '');\n }\n\n private updateControlAttribute(control: FieldControl, name: string, value: string | undefined) {\n if (value === undefined) {\n if (control.hasAttribute(name)) control.removeAttribute(name);\n return;\n }\n if (control.getAttribute(name) !== value) control.setAttribute(name, value);\n }\n\n private findControl(): FieldControl | undefined {\n return (\n this.controlContainer?.querySelector<FieldControl>(\n ':scope > :not(slot):not(.field__control-skeleton)'\n ) ?? undefined\n );\n }\n\n private observeLateControls() {\n if (this.childObserver || typeof MutationObserver === 'undefined') return;\n this.childObserver = new MutationObserver(records => {\n const addedControl = records.some(record =>\n Array.from(record.addedNodes).some(node => node.nodeType === 1)\n );\n if (addedControl) this.slotRevision++;\n });\n this.childObserver.observe(this.el, { childList: true });\n }\n\n private syncControl = () => {\n const control = this.findControl();\n if (!control) return;\n\n if (control !== this.control) {\n this.control = control;\n this.initialValue = this.readValue(control);\n this.authoredAria.set(control, {\n labelledby: tokens(control.getAttribute('aria-labelledby')),\n describedby: tokens(control.getAttribute('aria-describedby')),\n });\n this.observeControlAttributes(control);\n }\n\n this.constraintMessage = control.getAttribute('data-constraint-message') ?? '';\n this.characterCount = control.getAttribute('data-character-count') ?? undefined;\n this.characterLimit = control.getAttribute('data-character-limit') ?? undefined;\n\n const isDsControl = control.tagName.startsWith('DS-');\n if (isDsControl && 'inputId' in control) control.inputId = this.controlId;\n else this.updateControlAttribute(control, 'id', this.controlId);\n\n const authored = this.authoredAria.get(control) ?? { labelledby: [], describedby: [] };\n const labelledby = uniqueTokens(authored.labelledby, this.hasLabel ? [this.labelId] : []).join(\n ' '\n );\n const describedby = uniqueTokens(\n authored.describedby,\n this.renderedDescription ? [this.descriptionId] : [],\n this.renderedError ? [this.errorId] : [],\n this.characterCount !== undefined ? [`${this.controlId}-count`] : []\n ).join(' ');\n\n this.updateControlAttribute(control, 'aria-labelledby', labelledby || undefined);\n this.updateControlAttribute(control, 'aria-describedby', describedby || undefined);\n this.updateControlAttribute(control, 'aria-invalid', this.error ? 'true' : undefined);\n\n if (isDsControl && 'error' in control && control.error !== this.error)\n control.error = this.error;\n\n const value = this.readValue(control);\n this.filled = value.length > 0;\n this.dirty = value !== this.initialValue;\n this.controlDisabled = Boolean(\n control.disabled || control.isInactive || control.hasAttribute('disabled')\n );\n this.controlRequired = Boolean(control.required || control.hasAttribute('required'));\n this.controlBorderless = control.hasBorder === false;\n this.controlSize = readControlSize(control);\n };\n\n private observeControlAttributes(control: FieldControl) {\n this.controlAttrObserver?.disconnect();\n this.controlAttrObserver = new MutationObserver(() => this.syncControl());\n this.controlAttrObserver.observe(control, {\n attributes: true,\n attributeFilter: [\n 'has-border',\n 'size',\n 'class',\n 'data-constraint-message',\n 'data-character-count',\n 'data-character-limit',\n ],\n });\n }\n\n private handleLabelClick = (event: MouseEvent) => {\n if (this.isLoading) return;\n if (!this.control?.tagName.startsWith('DS-')) return;\n event.preventDefault();\n this.control.setFocus?.();\n };\n\n private handleFocusIn = () => {\n this.focused = true;\n };\n\n private handleFocusOut = (event: FocusEvent) => {\n if (event.relatedTarget instanceof Node && this.el.contains(event.relatedTarget)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleValueChange = () => {\n this.syncControl();\n };\n\n private renderCopy(value: string | undefined) {\n return [\n <span class={{ 'field__copy-source': true, 'field__copy-source--loading': this.isLoading }}>\n {value}\n </span>,\n this.isLoading && <ds-skeleton class=\"field__copy-skeleton\" textVariant=\"text-body-small\" />,\n ];\n }\n\n render() {\n return (\n <Host\n class={{ 'field--loading': this.isLoading }}\n aria-busy={this.isLoading ? 'true' : undefined}\n inert={this.isLoading}\n data-focused={this.focused ? '' : undefined}\n data-filled={this.filled ? '' : undefined}\n data-dirty={this.dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-invalid={this.error || this.constraintMessage ? '' : undefined}\n data-disabled={this.controlDisabled ? '' : undefined}\n data-required={this.controlRequired ? '' : undefined}\n onFocusin={this.handleFocusIn}\n onFocusout={this.handleFocusOut}\n onInput={this.handleValueChange}\n onChange={this.handleValueChange}\n onDsChange={this.handleValueChange}\n onDsTokensChange={this.handleValueChange}\n >\n <div\n class={{\n field: true,\n 'ds-field-stack': true,\n 'ds-field-stack--supporting-inset': this.controlBorderless,\n [`ds-control--${this.controlSize}`]: true,\n }}\n >\n {this.hasLabel ? (\n <ds-text\n class=\"field__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={this.controlId}\n textId={this.labelId}\n onClick={this.handleLabelClick}\n >\n {this.renderCopy(this.label)}\n </ds-text>\n ) : null}\n <div class=\"field__control\" ref={el => (this.controlContainer = el)}>\n <slot onSlotchange={this.syncControl} />\n {this.isLoading && (\n <ds-skeleton\n class=\"field__control-skeleton\"\n variant=\"control\"\n controlAppearance={this.controlBorderless ? 'borderless' : 'outlined'}\n controlContent={this.control?.localName === 'ds-select' ? 'label-icon' : 'label'}\n controlSize={this.controlSize}\n />\n )}\n </div>\n {(this.renderedDescription ||\n this.renderedError ||\n this.characterCount !== undefined) && (\n <div class=\"text-field-support\">\n {this.renderedDescription && (\n <ds-text\n class=\"field__description\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"secondary\"\n textId={this.descriptionId}\n >\n {this.renderCopy(this.description)}\n </ds-text>\n )}\n {this.renderedError && (\n <ds-text\n class=\"field__error\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.renderCopy(this.visibleError)}\n </ds-text>\n )}\n {this.characterCount !== undefined && (\n <ds-text\n class=\"text-field-count\"\n as=\"span\"\n variant=\"text-body-small\"\n color={this.error || this.constraintMessage ? 'negative' : 'secondary'}\n textId={`${this.controlId}-count`}\n >\n {this.renderCopy(`${this.characterCount}/${this.characterLimit}`)}\n </ds-text>\n )}\n </div>\n )}\n </div>\n </Host>\n );\n }\n}\n",
|
|
244
264
|
"type": "registry:ui"
|
|
245
265
|
}
|
|
246
266
|
]
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"states": [
|
|
49
49
|
"The description renders verbatim in every state in which the owning settings card projects the component.",
|
|
50
50
|
"The component has no internal visibility, editing, or dismissal state; the owning settings card controls whether it is present.",
|
|
51
|
-
"The content height is intrinsic: the banner contributes 8px block padding and its inner content container contributes 4px block padding. A single body-small line is 24px inside that container (4px + 16px line-height + 4px), followed by the tertiary bottom boundary."
|
|
51
|
+
"The content height is intrinsic: the banner contributes 8px block padding and its inner content container contributes 4px block padding. A single body-small line is 24px inside that container (4px + 16px line-height + 4px), followed by the tertiary bottom boundary.",
|
|
52
|
+
"isLoading replaces the copy with a wrapping text skeleton inside the original banner geometry."
|
|
52
53
|
],
|
|
53
54
|
"responsiveBehavior": [
|
|
54
55
|
"The component fills the available inline size and lets body-small copy wrap naturally.",
|
|
@@ -100,6 +101,15 @@
|
|
|
100
101
|
"required": true,
|
|
101
102
|
"mutable": false,
|
|
102
103
|
"description": "Authored informational copy rendered without transformation."
|
|
104
|
+
},
|
|
105
|
+
"isLoading": {
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"resolvedType": "boolean",
|
|
108
|
+
"attribute": "is-loading",
|
|
109
|
+
"default": "false",
|
|
110
|
+
"required": false,
|
|
111
|
+
"mutable": false,
|
|
112
|
+
"description": "Keep the informational row footprint while its copy is loading."
|
|
103
113
|
}
|
|
104
114
|
},
|
|
105
115
|
"events": [],
|
|
@@ -114,6 +124,15 @@
|
|
|
114
124
|
"required": true,
|
|
115
125
|
"mutable": false,
|
|
116
126
|
"description": "Authored informational copy rendered without transformation."
|
|
127
|
+
},
|
|
128
|
+
"isLoading": {
|
|
129
|
+
"type": "boolean",
|
|
130
|
+
"resolvedType": "boolean",
|
|
131
|
+
"attribute": "is-loading",
|
|
132
|
+
"default": "false",
|
|
133
|
+
"required": false,
|
|
134
|
+
"mutable": false,
|
|
135
|
+
"description": "Keep the informational row footprint while its copy is loading."
|
|
117
136
|
}
|
|
118
137
|
},
|
|
119
138
|
"events": [],
|
|
@@ -158,6 +177,7 @@
|
|
|
158
177
|
"@ds-mo/tokens"
|
|
159
178
|
],
|
|
160
179
|
"registryDependencies": [
|
|
180
|
+
"skeleton",
|
|
161
181
|
"text"
|
|
162
182
|
],
|
|
163
183
|
"files": [
|
|
@@ -168,7 +188,7 @@
|
|
|
168
188
|
},
|
|
169
189
|
{
|
|
170
190
|
"path": "src/wc/components/InlineBannerSettings/InlineBannerSettings.tsx",
|
|
171
|
-
"content": "import { Component, h, Host, Prop } from '@stencil/core';\n\n@Component({\n tag: 'ds-inline-banner-settings',\n styleUrl: 'InlineBannerSettings.css',\n scoped: true,\n})\nexport class InlineBannerSettings {\n /** Authored informational copy rendered without transformation. */\n @Prop() description!: string;\n\n render() {\n return (\n <Host
|
|
191
|
+
"content": "import { Component, h, Host, Prop } from '@stencil/core';\n\n@Component({\n tag: 'ds-inline-banner-settings',\n styleUrl: 'InlineBannerSettings.css',\n scoped: true,\n})\nexport class InlineBannerSettings {\n /** Authored informational copy rendered without transformation. */\n @Prop() description!: string;\n /** Keep the informational row footprint while its copy is loading. */\n @Prop() isLoading: boolean = false;\n\n render() {\n return (\n <Host aria-busy={this.isLoading ? 'true' : undefined}>\n <div class=\"inline-banner-settings__content\">\n <ds-skeleton preserveLayout isLoading={this.isLoading} textVariant=\"text-body-small\">\n <ds-text\n class=\"inline-banner-settings__description\"\n as=\"p\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {this.description}\n </ds-text>\n </ds-skeleton>\n </div>\n </Host>\n );\n }\n}\n",
|
|
172
192
|
"type": "registry:ui"
|
|
173
193
|
}
|
|
174
194
|
]
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"Zero results display Page 1 of 1. A partial final page still derives from totalItems. Assistive status keeps the complete item range.",
|
|
42
42
|
"loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests.",
|
|
43
43
|
"fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while page position reflects the resulting window. fitToPageInactive keeps that choice visible but unavailable when fitting is not meaningful for the current data shape. Pagination does not measure layouts itself.",
|
|
44
|
-
"showFirstLastButtons defaults to false. Enable it when direct jumps to both result boundaries materially help the workflow."
|
|
44
|
+
"showFirstLastButtons defaults to false. Enable it when direct jumps to both result boundaries materially help the workflow.",
|
|
45
|
+
"chromeLoading shows separate skeletons for the page-size label, selector, page position and each configured navigation control, retaining their intrinsic and responsive geometry. It also prevents requests."
|
|
45
46
|
],
|
|
46
47
|
"responsiveBehavior": [
|
|
47
48
|
"The full-width controls remain on one line with noun, selected size, a vertical divider, page position, Previous and Next, plus optional First and Last navigation.",
|
|
@@ -72,6 +73,15 @@
|
|
|
72
73
|
},
|
|
73
74
|
"api": {
|
|
74
75
|
"props": {
|
|
76
|
+
"chromeLoading": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"resolvedType": "boolean",
|
|
79
|
+
"attribute": "chrome-loading",
|
|
80
|
+
"default": "false",
|
|
81
|
+
"required": false,
|
|
82
|
+
"mutable": false,
|
|
83
|
+
"description": "Show skeletons for the existing controls without changing their geometry."
|
|
84
|
+
},
|
|
75
85
|
"fitPageSize": {
|
|
76
86
|
"type": "number | undefined",
|
|
77
87
|
"resolvedType": "number | undefined",
|
|
@@ -228,6 +238,15 @@
|
|
|
228
238
|
"slots": []
|
|
229
239
|
},
|
|
230
240
|
"props": {
|
|
241
|
+
"chromeLoading": {
|
|
242
|
+
"type": "boolean",
|
|
243
|
+
"resolvedType": "boolean",
|
|
244
|
+
"attribute": "chrome-loading",
|
|
245
|
+
"default": "false",
|
|
246
|
+
"required": false,
|
|
247
|
+
"mutable": false,
|
|
248
|
+
"description": "Show skeletons for the existing controls without changing their geometry."
|
|
249
|
+
},
|
|
231
250
|
"fitPageSize": {
|
|
232
251
|
"type": "number | undefined",
|
|
233
252
|
"resolvedType": "number | undefined",
|
|
@@ -425,6 +444,7 @@
|
|
|
425
444
|
"button-unfilled",
|
|
426
445
|
"divider",
|
|
427
446
|
"select",
|
|
447
|
+
"skeleton",
|
|
428
448
|
"text"
|
|
429
449
|
],
|
|
430
450
|
"files": [
|
|
@@ -435,7 +455,7 @@
|
|
|
435
455
|
},
|
|
436
456
|
{
|
|
437
457
|
"path": "src/wc/components/Pagination/Pagination.tsx",
|
|
438
|
-
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolvePaginationState } from './pagination-model';\nimport type { PaginationChangeDetail, PaginationPageSizeMode } from './pagination-types';\n\nlet paginationId = 0;\n\n@Component({\n tag: 'ds-pagination',\n styleUrl: 'Pagination.css',\n scoped: true,\n})\nexport class Pagination {\n @Element() el!: HTMLElement;\n\n /** Controlled zero-based page index. */\n @Prop() pageIndex: number = 0;\n /** Controlled number of top-level items on a full page. */\n @Prop() pageSize: number = 25;\n /** Whether pageSize is a fixed choice or a measured Fit snapshot. */\n @Prop() pageSizeMode: PaginationPageSizeMode = 'fixed';\n /** Controlled total number of top-level items across every page. */\n @Prop() totalItems: number = 0;\n /** Available page sizes. Assign arrays through JavaScript. */\n @Prop() pageSizeOptions: number[] = [25, 50, 100, 200];\n /** Include the Fit to page choice. */\n @Prop() fitToPage: boolean = false;\n /** Keep the Fit to page choice visible but unavailable. */\n @Prop() fitToPageInactive: boolean = false;\n /** Effective whole-item capacity to request when Fit is selected. */\n @Prop() fitPageSize: number | undefined;\n /** Full choice-list label for Fit. */\n @Prop() fitPageSizeLabel: string = 'Fit to page';\n /** Compact closed-trigger label while Fit is selected. */\n @Prop() fitPageSizeTriggerLabel: string = 'Fit';\n /** Localized plural noun used by assistive range announcements. */\n @Prop() itemLabel: string = 'items';\n /** Visible page-size control label. */\n @Prop() pageSizeLabel: string = 'Items';\n /** Accessible page-size control label. Defaults to “{pageSizeLabel} per page”. */\n @Prop() pageSizeAriaLabel: string | undefined;\n /** Accessible name for the pagination navigation region. */\n @Prop() label: string = 'Pagination';\n /** Include direct first-page and last-page controls. */\n @Prop() showFirstLastButtons: boolean = false;\n /** Prevent interaction while the owner replaces the current data page. */\n @Prop() loading: boolean = false;\n\n /** Emits the complete next controlled state after a page or page-size request. */\n @Event() dsChange!: EventEmitter<PaginationChangeDetail>;\n\n @State() private tableCompact = false;\n\n private readonly pageSizeControlId = `ds-pagination-${++paginationId}-page-size`;\n private tableCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.syncTableCompactObserver();\n }\n\n connectedCallback(): void {\n if (this.hasLoaded) this.syncTableCompactObserver();\n }\n\n disconnectedCallback(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = undefined;\n }\n\n private syncTableCompactObserver(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.tableCompact !== compact) this.tableCompact = compact;\n });\n }\n\n private requestPage(pageIndex: number): void {\n const state = this.resolvedState;\n if (\n this.loading ||\n pageIndex === state.pageIndex ||\n pageIndex < 0 ||\n pageIndex >= state.totalPages\n ) {\n return;\n }\n this.dsChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n showFirstLastButtons: this.showFirstLastButtons,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private requestPageSize(value: string | string[]): void {\n if (this.loading || typeof value !== 'string' || (value === 'fit' && this.fitToPageInactive))\n return;\n const state = this.resolvedState;\n const nextMode: PaginationPageSizeMode = value === 'fit' ? 'fit' : 'fixed';\n const pageSize = nextMode === 'fit' ? this.resolvedFitPageSize : Number(value);\n if (!pageSize || (pageSize === state.pageSize && nextMode === state.pageSizeMode)) return;\n this.dsChange.emit({\n pageIndex: 0,\n pageSize: Math.trunc(pageSize),\n pageSizeMode: nextMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n showFirstLastButtons: this.showFirstLastButtons,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page-size',\n });\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n ) {\n return;\n }\n const fromChoiceControl = event\n .composedPath()\n .some(\n node =>\n node instanceof HTMLElement &&\n (node.tagName === 'DS-SELECT' || ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName))\n );\n if (fromChoiceControl || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;\n const state = this.resolvedState;\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (this.loading || pageIndex < 0 || pageIndex >= state.totalPages) return;\n event.preventDefault();\n this.requestPage(pageIndex);\n }\n\n private get resolvedFitPageSize(): number | undefined {\n return Number.isFinite(this.fitPageSize) && Number(this.fitPageSize) > 0\n ? Math.trunc(Number(this.fitPageSize))\n : undefined;\n }\n\n private get resolvedState() {\n return resolvePaginationState({\n pageIndex: this.pageIndex,\n pageSize: this.pageSize,\n pageSizeMode: this.pageSizeMode,\n totalItems: this.totalItems,\n pageSizeOptions: this.pageSizeOptions,\n });\n }\n\n render() {\n const state = this.resolvedState;\n const atStart = state.pageIndex === 0;\n const atEnd = state.pageIndex === state.totalPages - 1;\n const page = `${state.pageIndex + 1} of ${state.totalPages}`;\n const announcedRange = `${state.firstItem}–${state.lastItem} of ${state.totalItems}`;\n const announcedPage = `Page ${state.pageIndex + 1} of ${state.totalPages}`;\n const pageSizeAriaLabel = this.pageSizeAriaLabel ?? `${this.pageSizeLabel} per page`;\n const options: Array<{ label: string; value: string; isInactive?: boolean }> =\n state.pageSizeOptions.map(value => ({ label: String(value), value: String(value) }));\n if (this.fitToPage) {\n options.push({\n label: this.fitPageSizeLabel,\n value: 'fit',\n isInactive: this.fitToPageInactive || this.resolvedFitPageSize === undefined,\n });\n }\n\n return (\n <Host>\n <nav\n class={{\n pagination: true,\n 'pagination--table-compact': this.tableCompact,\n }}\n aria-label={this.label}\n aria-busy={this.loading ? 'true' : undefined}\n onKeyDown={event => this.handleKeyDown(event)}\n >\n <div class=\"pagination__page-size\">\n <ds-text\n class=\"pagination__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {this.pageSizeLabel}:\n </ds-text>\n <span id={`${this.pageSizeControlId}-label`} class=\"ds-visually-hidden\">\n {pageSizeAriaLabel}\n </span>\n <ds-select\n class=\"pagination__page-size-select\"\n size=\"md\"\n inputId={this.pageSizeControlId}\n ariaLabelledby={`${this.pageSizeControlId}-label`}\n options={options}\n value={state.pageSizeMode === 'fit' ? 'fit' : String(state.pageSize)}\n triggerLabel={state.pageSizeMode === 'fit' ? this.fitPageSizeTriggerLabel : undefined}\n indicator=\"up-down\"\n allowClear={false}\n activeFill={false}\n hasBorder={false}\n isInactive={this.loading}\n onDsChange={event => {\n event.stopPropagation();\n this.requestPageSize(event.detail);\n }}\n />\n </div>\n <ds-divider\n class=\"pagination__divider\"\n orientation=\"vertical\"\n length=\"var(--dimension-size-250)\"\n />\n <div class=\"pagination__navigation\">\n {this.showFirstLastButtons ? (\n <ds-button-unfilled\n class=\"pagination__boundary\"\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeftDouble\"\n ariaLabel=\"First page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(0)}\n />\n ) : null}\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeft\"\n ariaLabel=\"Previous page\"\n hasBorder={false}\n isInactive={this.loading || atStart}\n onDsClick={() => this.requestPage(state.pageIndex - 1)}\n />\n <ds-text\n class=\"pagination__page\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {page}\n </ds-text>\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRight\"\n ariaLabel=\"Next page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.pageIndex + 1)}\n />\n {this.showFirstLastButtons ? (\n <ds-button-unfilled\n class=\"pagination__boundary\"\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRightDouble\"\n ariaLabel=\"Last page\"\n hasBorder={false}\n isInactive={this.loading || atEnd}\n onDsClick={() => this.requestPage(state.totalPages - 1)}\n />\n ) : null}\n </div>\n <span class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {announcedRange} {this.itemLabel}. {announcedPage}.\n </span>\n </nav>\n </Host>\n );\n }\n}\n",
|
|
458
|
+
"content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolvePaginationState } from './pagination-model';\nimport type { PaginationChangeDetail, PaginationPageSizeMode } from './pagination-types';\n\nlet paginationId = 0;\n\n@Component({\n tag: 'ds-pagination',\n styleUrl: 'Pagination.css',\n scoped: true,\n})\nexport class Pagination {\n @Element() el!: HTMLElement;\n\n /** Controlled zero-based page index. */\n @Prop() pageIndex: number = 0;\n /** Controlled number of top-level items on a full page. */\n @Prop() pageSize: number = 25;\n /** Whether pageSize is a fixed choice or a measured Fit snapshot. */\n @Prop() pageSizeMode: PaginationPageSizeMode = 'fixed';\n /** Controlled total number of top-level items across every page. */\n @Prop() totalItems: number = 0;\n /** Available page sizes. Assign arrays through JavaScript. */\n @Prop() pageSizeOptions: number[] = [25, 50, 100, 200];\n /** Include the Fit to page choice. */\n @Prop() fitToPage: boolean = false;\n /** Keep the Fit to page choice visible but unavailable. */\n @Prop() fitToPageInactive: boolean = false;\n /** Effective whole-item capacity to request when Fit is selected. */\n @Prop() fitPageSize: number | undefined;\n /** Full choice-list label for Fit. */\n @Prop() fitPageSizeLabel: string = 'Fit to page';\n /** Compact closed-trigger label while Fit is selected. */\n @Prop() fitPageSizeTriggerLabel: string = 'Fit';\n /** Localized plural noun used by assistive range announcements. */\n @Prop() itemLabel: string = 'items';\n /** Visible page-size control label. */\n @Prop() pageSizeLabel: string = 'Items';\n /** Accessible page-size control label. Defaults to “{pageSizeLabel} per page”. */\n @Prop() pageSizeAriaLabel: string | undefined;\n /** Accessible name for the pagination navigation region. */\n @Prop() label: string = 'Pagination';\n /** Include direct first-page and last-page controls. */\n @Prop() showFirstLastButtons: boolean = false;\n /** Prevent interaction while the owner replaces the current data page. */\n @Prop() loading: boolean = false;\n /** Show skeletons for the existing controls without changing their geometry. */\n @Prop() chromeLoading: boolean = false;\n\n private get busy(): boolean {\n return this.loading || this.chromeLoading;\n }\n\n /** Emits the complete next controlled state after a page or page-size request. */\n @Event() dsChange!: EventEmitter<PaginationChangeDetail>;\n\n @State() private tableCompact = false;\n\n private readonly pageSizeControlId = `ds-pagination-${++paginationId}-page-size`;\n private tableCompactDisconnect: (() => void) | undefined;\n private hasLoaded = false;\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.syncTableCompactObserver();\n }\n\n connectedCallback(): void {\n if (this.hasLoaded) this.syncTableCompactObserver();\n }\n\n disconnectedCallback(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = undefined;\n }\n\n private syncTableCompactObserver(): void {\n this.tableCompactDisconnect?.();\n this.tableCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.tableCompact !== compact) this.tableCompact = compact;\n });\n }\n\n private requestPage(pageIndex: number): void {\n const state = this.resolvedState;\n if (\n this.busy ||\n pageIndex === state.pageIndex ||\n pageIndex < 0 ||\n pageIndex >= state.totalPages\n ) {\n return;\n }\n this.dsChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n showFirstLastButtons: this.showFirstLastButtons,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private requestPageSize(value: string | string[]): void {\n if (this.busy || typeof value !== 'string' || (value === 'fit' && this.fitToPageInactive))\n return;\n const state = this.resolvedState;\n const nextMode: PaginationPageSizeMode = value === 'fit' ? 'fit' : 'fixed';\n const pageSize = nextMode === 'fit' ? this.resolvedFitPageSize : Number(value);\n if (!pageSize || (pageSize === state.pageSize && nextMode === state.pageSizeMode)) return;\n this.dsChange.emit({\n pageIndex: 0,\n pageSize: Math.trunc(pageSize),\n pageSizeMode: nextMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: this.fitToPage,\n fitToPageInactive: this.fitToPageInactive,\n fitPageSize: this.resolvedFitPageSize,\n fitPageSizeLabel: this.fitPageSizeLabel,\n fitPageSizeTriggerLabel: this.fitPageSizeTriggerLabel,\n itemLabel: this.itemLabel,\n pageSizeLabel: this.pageSizeLabel,\n ariaLabel: this.label,\n showFirstLastButtons: this.showFirstLastButtons,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page-size',\n });\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n ) {\n return;\n }\n const fromChoiceControl = event\n .composedPath()\n .some(\n node =>\n node instanceof HTMLElement &&\n (node.tagName === 'DS-SELECT' || ['INPUT', 'SELECT', 'TEXTAREA'].includes(node.tagName))\n );\n if (fromChoiceControl || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) return;\n const state = this.resolvedState;\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (this.busy || pageIndex < 0 || pageIndex >= state.totalPages) return;\n event.preventDefault();\n this.requestPage(pageIndex);\n }\n\n private get resolvedFitPageSize(): number | undefined {\n return Number.isFinite(this.fitPageSize) && Number(this.fitPageSize) > 0\n ? Math.trunc(Number(this.fitPageSize))\n : undefined;\n }\n\n private get resolvedState() {\n return resolvePaginationState({\n pageIndex: this.pageIndex,\n pageSize: this.pageSize,\n pageSizeMode: this.pageSizeMode,\n totalItems: this.totalItems,\n pageSizeOptions: this.pageSizeOptions,\n });\n }\n\n render() {\n const state = this.resolvedState;\n const atStart = state.pageIndex === 0;\n const atEnd = state.pageIndex === state.totalPages - 1;\n const page = `${state.pageIndex + 1} of ${state.totalPages}`;\n const announcedRange = `${state.firstItem}–${state.lastItem} of ${state.totalItems}`;\n const announcedPage = `Page ${state.pageIndex + 1} of ${state.totalPages}`;\n const pageSizeAriaLabel = this.pageSizeAriaLabel ?? `${this.pageSizeLabel} per page`;\n const options: Array<{ label: string; value: string; isInactive?: boolean }> =\n state.pageSizeOptions.map(value => ({ label: String(value), value: String(value) }));\n if (this.fitToPage) {\n options.push({\n label: this.fitPageSizeLabel,\n value: 'fit',\n isInactive: this.fitToPageInactive || this.resolvedFitPageSize === undefined,\n });\n }\n\n return (\n <Host>\n <nav\n class={{\n pagination: true,\n 'pagination--table-compact': this.tableCompact,\n }}\n aria-label={this.label}\n aria-busy={this.busy ? 'true' : undefined}\n onKeyDown={event => this.handleKeyDown(event)}\n >\n <div class=\"pagination__page-size\">\n <ds-skeleton preserveLayout={true} isLoading={this.chromeLoading}>\n <ds-text\n class=\"pagination__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {this.pageSizeLabel}:\n </ds-text>\n </ds-skeleton>\n <span id={`${this.pageSizeControlId}-label`} class=\"ds-visually-hidden\">\n {pageSizeAriaLabel}\n </span>\n <ds-skeleton\n preserveLayout={true}\n isLoading={this.chromeLoading}\n variant=\"control\"\n controlAppearance=\"borderless\"\n controlContent=\"label-icon\"\n >\n <ds-select\n class=\"pagination__page-size-select\"\n size=\"md\"\n inputId={this.pageSizeControlId}\n ariaLabelledby={`${this.pageSizeControlId}-label`}\n options={options}\n value={state.pageSizeMode === 'fit' ? 'fit' : String(state.pageSize)}\n triggerLabel={\n state.pageSizeMode === 'fit' ? this.fitPageSizeTriggerLabel : undefined\n }\n indicator=\"up-down\"\n allowClear={false}\n activeFill={false}\n hasBorder={false}\n isInactive={this.busy}\n onDsChange={event => {\n event.stopPropagation();\n this.requestPageSize(event.detail);\n }}\n />\n </ds-skeleton>\n </div>\n <ds-divider\n class=\"pagination__divider\"\n orientation=\"vertical\"\n length=\"var(--dimension-size-250)\"\n />\n <div class=\"pagination__navigation\">\n {this.showFirstLastButtons ? (\n <ds-skeleton\n class=\"pagination__boundary\"\n preserveLayout={true}\n isLoading={this.chromeLoading}\n variant=\"control\"\n controlAppearance=\"borderless\"\n controlContent=\"icon\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeftDouble\"\n ariaLabel=\"First page\"\n hasBorder={false}\n isInactive={this.busy || atStart}\n onDsClick={() => this.requestPage(0)}\n />\n </ds-skeleton>\n ) : null}\n <ds-skeleton\n preserveLayout={true}\n isLoading={this.chromeLoading}\n variant=\"control\"\n controlAppearance=\"borderless\"\n controlContent=\"icon\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronLeft\"\n ariaLabel=\"Previous page\"\n hasBorder={false}\n isInactive={this.busy || atStart}\n onDsClick={() => this.requestPage(state.pageIndex - 1)}\n />\n </ds-skeleton>\n <ds-skeleton preserveLayout={true} isLoading={this.chromeLoading}>\n <ds-text\n class=\"pagination__page\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n fontFeature=\"tabular-nums\"\n >\n {page}\n </ds-text>\n </ds-skeleton>\n <ds-skeleton\n preserveLayout={true}\n isLoading={this.chromeLoading}\n variant=\"control\"\n controlAppearance=\"borderless\"\n controlContent=\"icon\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRight\"\n ariaLabel=\"Next page\"\n hasBorder={false}\n isInactive={this.busy || atEnd}\n onDsClick={() => this.requestPage(state.pageIndex + 1)}\n />\n </ds-skeleton>\n {this.showFirstLastButtons ? (\n <ds-skeleton\n class=\"pagination__boundary\"\n preserveLayout={true}\n isLoading={this.chromeLoading}\n variant=\"control\"\n controlAppearance=\"borderless\"\n controlContent=\"icon\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n size=\"md\"\n icon=\"ChevronRightDouble\"\n ariaLabel=\"Last page\"\n hasBorder={false}\n isInactive={this.busy || atEnd}\n onDsClick={() => this.requestPage(state.totalPages - 1)}\n />\n </ds-skeleton>\n ) : null}\n </div>\n <span class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.chromeLoading\n ? 'Loading pages'\n : `${announcedRange} ${this.itemLabel}. ${announcedPage}.`}\n </span>\n </nav>\n </Host>\n );\n }\n}\n",
|
|
439
459
|
"type": "registry:ui"
|
|
440
460
|
},
|
|
441
461
|
{
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"Description-rich options grow beyond the fixed density height while retaining its minimum target size and primitive-owned selection anatomy.",
|
|
62
62
|
"A visible group label uses the same density height and horizontal text origin as a menu section heading above the option list without adding a second interaction target. Copy uses text-body-small emphasis.",
|
|
63
63
|
"Vertical form options use a 4px gap; a medium single-line option and its group label each use the shared 32px control height.",
|
|
64
|
-
"Form and settings usage omits a row hover or pressed wash by default; set hasInteractionFill only when each radio option is presented as a choice-list row."
|
|
64
|
+
"Form and settings usage omits a row hover or pressed wash by default; set hasInteractionFill only when each radio option is presented as a choice-list row.",
|
|
65
|
+
"isLoading preserves option geometry and the submitted form value while skeletonizing the circles and copy. Loading options cannot receive focus or change selection."
|
|
65
66
|
],
|
|
66
67
|
"responsiveBehavior": [
|
|
67
68
|
"Choose size from the density of the containing form or settings row.",
|
|
@@ -200,6 +201,15 @@
|
|
|
200
201
|
"mutable": false,
|
|
201
202
|
"description": "Design-system inactive state for the complete set."
|
|
202
203
|
},
|
|
204
|
+
"isLoading": {
|
|
205
|
+
"type": "boolean",
|
|
206
|
+
"resolvedType": "boolean",
|
|
207
|
+
"attribute": "is-loading",
|
|
208
|
+
"default": "false",
|
|
209
|
+
"required": false,
|
|
210
|
+
"mutable": false,
|
|
211
|
+
"description": "Preserve each option while replacing its circle and copy with loading atoms."
|
|
212
|
+
},
|
|
203
213
|
"name": {
|
|
204
214
|
"type": "string | undefined",
|
|
205
215
|
"resolvedType": "string | undefined",
|
|
@@ -343,6 +353,15 @@
|
|
|
343
353
|
"mutable": false,
|
|
344
354
|
"description": "Design-system inactive state for the complete set."
|
|
345
355
|
},
|
|
356
|
+
"isLoading": {
|
|
357
|
+
"type": "boolean",
|
|
358
|
+
"resolvedType": "boolean",
|
|
359
|
+
"attribute": "is-loading",
|
|
360
|
+
"default": "false",
|
|
361
|
+
"required": false,
|
|
362
|
+
"mutable": false,
|
|
363
|
+
"description": "Preserve each option while replacing its circle and copy with loading atoms."
|
|
364
|
+
},
|
|
346
365
|
"name": {
|
|
347
366
|
"type": "string | undefined",
|
|
348
367
|
"resolvedType": "string | undefined",
|
|
@@ -454,6 +473,7 @@
|
|
|
454
473
|
"@ds-mo/tokens"
|
|
455
474
|
],
|
|
456
475
|
"registryDependencies": [
|
|
476
|
+
"skeleton",
|
|
457
477
|
"text"
|
|
458
478
|
],
|
|
459
479
|
"files": [
|
|
@@ -464,7 +484,7 @@
|
|
|
464
484
|
},
|
|
465
485
|
{
|
|
466
486
|
"path": "src/wc/components/Radio/Radio.tsx",
|
|
467
|
-
"content": "import {\n AttachInternals,\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Listen,\n Method,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport interface RadioOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport type RadioSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet radioIdCounter = 0;\n\n@Component({\n tag: 'ds-radio',\n styleUrl: 'Radio.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Radio {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private readonly instanceId = ++radioIdCounter;\n\n /** Visible choices in this one-of-many set. */\n @Prop() options: RadioOption[] = [];\n /** Optional visible group label rendered above the options. */\n @Prop() groupLabel: string = '';\n /** Selected option value. */\n @Prop({ mutable: true }) value: string = '';\n /** Visual and placement density for every option. */\n @Prop() size: RadioSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Associates the radio set with a form by id when rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n /** Native disabled state for the complete set. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one option to be selected for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required set has no selection. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Layout direction for the option set. */\n @Prop() direction: 'vertical' | 'horizontal' = 'vertical';\n /** Design-system inactive state for the complete set. */\n @Prop() isInactive: boolean = false;\n /** Show full-row hover and pressed feedback for choice-list presentations. */\n @Prop() hasInteractionFill: boolean = false;\n /** Accessible name when visible group labeling is unavailable. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id reference for a visible group label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n\n /** Emitted after user selection with the selected option value. */\n @Event() dsChange!: EventEmitter<string>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n\n private get hasGroupLabel(): boolean {\n return Boolean(this.groupLabel.trim());\n }\n\n private get hasSelection(): boolean {\n return this.options.some(option => option.value === this.value && !option.isInactive);\n }\n\n /** Settings rows own a separate heading; never mount the form group label there. */\n private get showGroupLabel(): boolean {\n return this.hasGroupLabel && !this.el.closest('ds-setting-row-radio');\n }\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n\n @Watch('value')\n @Watch('options')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('requiredMessage')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.hasSelection ? this.value : '', {\n inactive,\n state: this.value,\n });\n const missing = this.required && !inactive && !this.hasSelection;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n /** Focus the selected option, or the first active option when nothing is selected. */\n @Method()\n async setFocus() {\n this.activeItems.find(item => item.tabIndex === 0)?.focus();\n }\n\n private get activeItems(): HTMLElement[] {\n return Array.from(\n this.el.querySelectorAll<HTMLElement>('[data-radio-item]:not([data-inactive])')\n );\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n const items = this.activeItems;\n if (!items.length) return;\n\n const focused = items.find(item => item === document.activeElement);\n if (!focused) return;\n\n const idx = items.indexOf(focused);\n let next = -1;\n\n switch (e.key) {\n case 'ArrowDown':\n case 'ArrowRight':\n e.preventDefault();\n next = (idx + 1) % items.length;\n break;\n case 'ArrowUp':\n case 'ArrowLeft':\n e.preventDefault();\n next = (idx - 1 + items.length) % items.length;\n break;\n case 'Home':\n e.preventDefault();\n next = 0;\n break;\n case 'End':\n e.preventDefault();\n next = items.length - 1;\n break;\n }\n\n if (next >= 0) {\n items[next].focus();\n const nextValue = items[next].dataset['value'];\n if (nextValue !== undefined && nextValue !== this.value) {\n this.value = nextValue;\n this.dsChange.emit(nextValue);\n }\n }\n }\n\n private selectItem(optValue: string) {\n if (this.isInactive || this.disabled || this.formDisabled) return;\n if (optValue !== this.value) {\n this.value = optValue;\n this.dsChange.emit(optValue);\n }\n }\n\n private renderOptions(inactive: boolean, focusableIdx: number) {\n return this.options.map((option, index) => {\n const isItemInactive = inactive || !!option.isInactive;\n const isChecked = option.value === this.value;\n const tabIdx = isItemInactive ? -1 : index === focusableIdx ? 0 : -1;\n const labelId = `ds-radio-${this.instanceId}-label-${index}`;\n const descriptionId = `ds-radio-${this.instanceId}-description-${index}`;\n\n return (\n <div\n key={option.value}\n role=\"radio\"\n aria-checked={String(isChecked)}\n aria-disabled={isItemInactive ? 'true' : undefined}\n aria-labelledby={labelId}\n aria-describedby={option.description ? descriptionId : undefined}\n tabIndex={tabIdx}\n data-radio-item\n data-value={option.value}\n data-inactive={isItemInactive || undefined}\n class={{\n radio__item: true,\n 'radio__item--described': Boolean(option.description),\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': isItemInactive,\n 'ds-focus-ring-inset': !isItemInactive,\n 'ds-interaction-fill': !isItemInactive && this.hasInteractionFill,\n }}\n onClick={() => !isItemInactive && this.selectItem(option.value)}\n onKeyDown={(e: KeyboardEvent) => {\n if ((e.key === ' ' || e.key === 'Enter') && !isItemInactive) {\n e.preventDefault();\n this.selectItem(option.value);\n }\n }}\n >\n <span class=\"radio__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ radio__circle: true, 'radio__circle--checked': isChecked }}>\n {isChecked && <span class=\"radio__dot\" />}\n </span>\n </span>\n <span class=\"radio__copy ds-interaction-fill__content\">\n <ds-text\n class=\"radio__label\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n textId={labelId}\n >\n {option.label}\n </ds-text>\n {option.description ? (\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n textId={descriptionId}\n >\n {option.description}\n </ds-text>\n ) : null}\n </span>\n </div>\n );\n });\n }\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.hasSelection;\n const selectedIdx = this.options.findIndex(option => option.value === this.value);\n const firstActiveIdx = this.options.findIndex(option => !inactive && !option.isInactive);\n const focusableIdx =\n selectedIdx >= 0 && !inactive && !this.options[selectedIdx]?.isInactive\n ? selectedIdx\n : firstActiveIdx;\n const renderedOptions = this.renderOptions(inactive, focusableIdx);\n\n return (\n <Host\n role=\"radiogroup\"\n aria-label={this.showGroupLabel ? this.groupLabel : this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-required={this.required ? 'true' : undefined}\n aria-invalid={invalid ? 'true' : undefined}\n class={{\n radio: true,\n 'radio--horizontal': this.direction === 'horizontal',\n [`radio--${this.size}`]: true,\n 'radio--labeled': this.showGroupLabel,\n }}\n >\n {this.showGroupLabel ? (\n <ds-text\n key=\"radio-group-label\"\n class={`radio__group-label ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n >\n {this.groupLabel}\n </ds-text>\n ) : null}\n <div key=\"radio-options\" class=\"radio__options\">\n {renderedOptions}\n </div>\n </Host>\n );\n }\n}\n",
|
|
487
|
+
"content": "import {\n AttachInternals,\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Listen,\n Method,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport interface RadioOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport type RadioSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet radioIdCounter = 0;\n\n@Component({\n tag: 'ds-radio',\n styleUrl: 'Radio.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Radio {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private readonly instanceId = ++radioIdCounter;\n\n /** Visible choices in this one-of-many set. */\n @Prop() options: RadioOption[] = [];\n /** Optional visible group label rendered above the options. */\n @Prop() groupLabel: string = '';\n /** Selected option value. */\n @Prop({ mutable: true }) value: string = '';\n /** Visual and placement density for every option. */\n @Prop() size: RadioSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Associates the radio set with a form by id when rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n /** Native disabled state for the complete set. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one option to be selected for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required set has no selection. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Layout direction for the option set. */\n @Prop() direction: 'vertical' | 'horizontal' = 'vertical';\n /** Design-system inactive state for the complete set. */\n @Prop() isInactive: boolean = false;\n /** Preserve each option while replacing its circle and copy with loading atoms. */\n @Prop() isLoading: boolean = false;\n /** Show full-row hover and pressed feedback for choice-list presentations. */\n @Prop() hasInteractionFill: boolean = false;\n /** Accessible name when visible group labeling is unavailable. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id reference for a visible group label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n\n /** Emitted after user selection with the selected option value. */\n @Event() dsChange!: EventEmitter<string>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n\n private get hasGroupLabel(): boolean {\n return Boolean(this.groupLabel.trim());\n }\n\n private get hasSelection(): boolean {\n return this.options.some(option => option.value === this.value && !option.isInactive);\n }\n\n /** Settings rows own a separate heading; never mount the form group label there. */\n private get showGroupLabel(): boolean {\n return this.hasGroupLabel && !this.el.closest('ds-setting-row-radio');\n }\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n\n @Watch('value')\n @Watch('options')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('requiredMessage')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.hasSelection ? this.value : '', {\n inactive,\n state: this.value,\n });\n const missing = this.required && !inactive && !this.hasSelection;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n /** Focus the selected option, or the first active option when nothing is selected. */\n @Method()\n async setFocus() {\n this.activeItems.find(item => item.tabIndex === 0)?.focus();\n }\n\n private get activeItems(): HTMLElement[] {\n return Array.from(\n this.el.querySelectorAll<HTMLElement>('[data-radio-item]:not([data-inactive])')\n );\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (this.isLoading) return;\n const items = this.activeItems;\n if (!items.length) return;\n\n const focused = items.find(item => item === document.activeElement);\n if (!focused) return;\n\n const idx = items.indexOf(focused);\n let next = -1;\n\n switch (e.key) {\n case 'ArrowDown':\n case 'ArrowRight':\n e.preventDefault();\n next = (idx + 1) % items.length;\n break;\n case 'ArrowUp':\n case 'ArrowLeft':\n e.preventDefault();\n next = (idx - 1 + items.length) % items.length;\n break;\n case 'Home':\n e.preventDefault();\n next = 0;\n break;\n case 'End':\n e.preventDefault();\n next = items.length - 1;\n break;\n }\n\n if (next >= 0) {\n items[next].focus();\n const nextValue = items[next].dataset['value'];\n if (nextValue !== undefined && nextValue !== this.value) {\n this.value = nextValue;\n this.dsChange.emit(nextValue);\n }\n }\n }\n\n private selectItem(optValue: string) {\n if (this.isLoading || this.isInactive || this.disabled || this.formDisabled) return;\n if (optValue !== this.value) {\n this.value = optValue;\n this.dsChange.emit(optValue);\n }\n }\n\n private renderOptions(inactive: boolean, focusableIdx: number) {\n return this.options.map((option, index) => {\n const isItemInactive = inactive || !!option.isInactive;\n const isChecked = option.value === this.value;\n const tabIdx = isItemInactive ? -1 : index === focusableIdx ? 0 : -1;\n const labelId = `ds-radio-${this.instanceId}-label-${index}`;\n const descriptionId = `ds-radio-${this.instanceId}-description-${index}`;\n\n return (\n <div\n key={option.value}\n role=\"radio\"\n aria-checked={String(isChecked)}\n aria-disabled={isItemInactive ? 'true' : undefined}\n aria-labelledby={labelId}\n aria-describedby={option.description ? descriptionId : undefined}\n tabIndex={this.isLoading ? -1 : tabIdx}\n aria-hidden={this.isLoading ? 'true' : undefined}\n inert={this.isLoading}\n data-radio-item\n data-value={option.value}\n data-inactive={isItemInactive || undefined}\n class={{\n radio__item: true,\n 'radio__item--described': Boolean(option.description),\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': isItemInactive,\n 'ds-focus-ring-inset': !isItemInactive,\n 'ds-interaction-fill': !isItemInactive && this.hasInteractionFill,\n }}\n onClick={() => !isItemInactive && this.selectItem(option.value)}\n onKeyDown={(e: KeyboardEvent) => {\n if ((e.key === ' ' || e.key === 'Enter') && !isItemInactive) {\n e.preventDefault();\n this.selectItem(option.value);\n }\n }}\n >\n <span class=\"radio__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-skeleton preserveLayout isLoading={this.isLoading} variant=\"control\" rounded>\n <span class={{ radio__circle: true, 'radio__circle--checked': isChecked }}>\n {isChecked && <span class=\"radio__dot\" />}\n </span>\n </ds-skeleton>\n </span>\n <span class=\"radio__copy ds-interaction-fill__content\">\n <ds-skeleton\n preserveLayout\n isLoading={this.isLoading}\n textVariant={CONTROL_TEXT_VARIANT[this.size]}\n >\n <ds-text\n class=\"radio__label\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n textId={labelId}\n >\n {option.label}\n </ds-text>\n </ds-skeleton>\n {option.description ? (\n <ds-skeleton\n preserveLayout\n isLoading={this.isLoading}\n textVariant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n >\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n textId={descriptionId}\n >\n {option.description}\n </ds-text>\n </ds-skeleton>\n ) : null}\n </span>\n </div>\n );\n });\n }\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.hasSelection;\n const selectedIdx = this.options.findIndex(option => option.value === this.value);\n const firstActiveIdx = this.options.findIndex(option => !inactive && !option.isInactive);\n const focusableIdx =\n selectedIdx >= 0 && !inactive && !this.options[selectedIdx]?.isInactive\n ? selectedIdx\n : firstActiveIdx;\n const renderedOptions = this.renderOptions(inactive, focusableIdx);\n\n return (\n <Host\n role=\"radiogroup\"\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-label={this.showGroupLabel ? this.groupLabel : this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-required={this.required ? 'true' : undefined}\n aria-invalid={invalid ? 'true' : undefined}\n class={{\n radio: true,\n 'radio--horizontal': this.direction === 'horizontal',\n [`radio--${this.size}`]: true,\n 'radio--labeled': this.showGroupLabel,\n }}\n >\n {this.showGroupLabel ? (\n <ds-skeleton preserveLayout isLoading={this.isLoading} textVariant={'text-body-small'}>\n <ds-text\n key=\"radio-group-label\"\n class={`radio__group-label ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n >\n {this.groupLabel}\n </ds-text>\n </ds-skeleton>\n ) : null}\n <div key=\"radio-options\" class=\"radio__options\">\n {renderedOptions}\n </div>\n </Host>\n );\n }\n}\n",
|
|
468
488
|
"type": "registry:ui"
|
|
469
489
|
}
|
|
470
490
|
]
|