From c0c9c48cd7c5383bba7fbb85dd2d1a48c77bfd9a Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Mon, 3 Feb 2025 10:42:38 -0500 Subject: [PATCH 1/2] feat: create @skyux/code-examples NPM package (internal use only) (#3091) --- .github/workflows/e2e.yml | 4 +- .github/workflows/validate-pr.yml | 3 +- .skyuxdev.json | 1 + .vscode/tasks.json | 4 +- apps/code-examples/project.json | 10 +- libs/components/code-examples/README.md | 1 + .../components/code-examples/eslint.config.js | 41 ++ libs/components/code-examples/karma.conf.js | 25 ++ libs/components/code-examples/ng-package.json | 9 + libs/components/code-examples/package.json | 64 ++++ libs/components/code-examples/project.json | 66 ++++ libs/components/code-examples/src/index.ts | 169 +++++++++ .../basic/example.component.html | 35 ++ .../basic/example.component.ts | 22 ++ .../modal/example.component.html | 3 + .../modal/example.component.ts | 19 + .../modal/modal.component.html | 41 ++ .../modal/modal.component.ts | 29 ++ .../tab/example.component.html | 41 ++ .../tab/example.component.ts | 23 ++ .../basic/context-menu.component.html | 31 ++ .../basic/context-menu.component.ts | 38 ++ .../ag-grid/data-entry-grid/basic/data.ts | 156 ++++++++ .../basic/edit-modal-context.ts | 5 + .../basic/edit-modal.component.html | 30 ++ .../basic/edit-modal.component.ts | 236 ++++++++++++ .../basic/example.component.html | 23 ++ .../basic/example.component.ts | 209 +++++++++++ .../basic/mark-inactive.component.html | 8 + .../basic/mark-inactive.component.ts | 29 ++ .../context-menu.component.html | 31 ++ .../context-menu.component.ts | 38 ++ .../data-manager-added/data.ts | 157 ++++++++ .../data-manager-added/edit-modal-context.ts | 5 + .../edit-modal.component.html | 27 ++ .../edit-modal.component.ts | 193 ++++++++++ .../data-manager-added/example.component.html | 14 + .../data-manager-added/example.component.ts | 141 +++++++ .../filter-modal.component.html | 37 ++ .../filter-modal.component.ts | 65 ++++ .../data-manager-added/filters.ts | 4 + .../view-grid.component.html | 12 + .../data-manager-added/view-grid.component.ts | 352 ++++++++++++++++++ .../ag-grid/data-entry-grid/focus/data.ts | 156 ++++++++ .../focus/example.component.html | 23 ++ .../focus/example.component.ts | 114 ++++++ .../inline-help/context-menu.component.html | 31 ++ .../inline-help/context-menu.component.ts | 38 ++ .../data-entry-grid/inline-help/data.ts | 156 ++++++++ .../inline-help/edit-modal-context.ts | 5 + .../inline-help/edit-modal.component.html | 27 ++ .../inline-help/edit-modal.component.ts | 207 ++++++++++ .../inline-help/example.component.html | 24 ++ .../inline-help/example.component.ts | 232 ++++++++++++ .../inline-help/inline-help.component.html | 5 + .../inline-help/inline-help.component.ts | 30 ++ .../context-menu.component.html | 31 ++ .../context-menu.component.ts | 38 ++ .../data-grid/basic-multiselect/data.ts | 156 ++++++++ .../basic-multiselect/example.component.html | 3 + .../basic-multiselect/example.component.ts | 94 +++++ .../basic/context-menu.component.html | 31 ++ .../data-grid/basic/context-menu.component.ts | 38 ++ .../modules/ag-grid/data-grid/basic/data.ts | 149 ++++++++ .../data-grid/basic/example.component.html | 3 + .../data-grid/basic/example.component.ts | 86 +++++ .../context-menu.component.html | 31 ++ .../context-menu.component.ts | 38 ++ .../data-manager-multiselect/data.ts | 157 ++++++++ .../example.component.html | 4 + .../example.component.ts | 106 ++++++ .../filter-modal.component.html | 37 ++ .../filter-modal.component.ts | 65 ++++ .../data-manager-multiselect/filters.ts | 4 + .../view-grid.component.html | 12 + .../view-grid.component.ts | 340 +++++++++++++++++ .../data-manager/context-menu.component.html | 31 ++ .../data-manager/context-menu.component.ts | 38 ++ .../ag-grid/data-grid/data-manager/data.ts | 150 ++++++++ .../data-manager/example.component.html | 4 + .../data-manager/example.component.ts | 105 ++++++ .../data-manager/filter-modal.component.html | 37 ++ .../data-manager/filter-modal.component.ts | 65 ++++ .../ag-grid/data-grid/data-manager/filters.ts | 4 + .../data-manager/view-grid.component.html | 12 + .../data-manager/view-grid.component.ts | 315 ++++++++++++++++ .../inline-help/context-menu.component.html | 31 ++ .../inline-help/context-menu.component.ts | 38 ++ .../ag-grid/data-grid/inline-help/data.ts | 149 ++++++++ .../inline-help/example.component.html | 19 + .../inline-help/example.component.ts | 157 ++++++++ .../inline-help/inline-help.component.html | 5 + .../inline-help/inline-help.component.ts | 29 ++ .../paging/context-menu.component.html | 31 ++ .../paging/context-menu.component.ts | 38 ++ .../modules/ag-grid/data-grid/paging/data.ts | 149 ++++++++ .../data-grid/paging/example.component.html | 10 + .../data-grid/paging/example.component.ts | 163 ++++++++ .../data-grid/template-ref-column/data.ts | 58 +++ .../example.component.html | 14 + .../template-ref-column/example.component.ts | 70 ++++ .../top-scroll/context-menu.component.html | 31 ++ .../top-scroll/context-menu.component.ts | 38 ++ .../ag-grid/data-grid/top-scroll/data.ts | 156 ++++++++ .../top-scroll/example.component.html | 18 + .../data-grid/top-scroll/example.component.ts | 128 +++++++ .../angular-tree/basic/example.component.html | 18 + .../angular-tree/basic/example.component.ts | 45 +++ .../help-key/example.component.html | 20 + .../help-key/example.component.ts | 45 +++ .../currency/example.component.html | 9 + .../autonumeric/currency/example.component.ts | 38 ++ .../example.component.html | 9 + .../example.component.ts | 36 ++ .../options-provider/example.component.html | 16 + .../options-provider/example.component.ts | 44 +++ .../options-provider/options-provider.ts | 22 ++ .../autonumeric/preset/example.component.html | 9 + .../autonumeric/preset/example.component.ts | 30 ++ .../avatar/avatar/example.component.html | 8 + .../avatar/avatar/example.component.spec.ts | 89 +++++ .../avatar/avatar/example.component.ts | 31 ++ .../modules/avatar/avatar/example.service.ts | 14 + .../colorpicker/basic/example.component.html | 25 ++ .../basic/example.component.spec.ts | 58 +++ .../colorpicker/basic/example.component.ts | 67 ++++ .../help-key/example.component.html | 26 ++ .../help-key/example.component.spec.ts | 74 ++++ .../colorpicker/help-key/example.component.ts | 67 ++++ .../programmatic/example.component.html | 46 +++ .../programmatic/example.component.ts | 71 ++++ .../modules/core/id/example.component.html | 1 + .../lib/modules/core/id/example.component.ts | 9 + .../media-query/basic/example.component.html | 21 ++ .../basic/example.component.spec.ts | 43 +++ .../media-query/basic/example.component.ts | 16 + .../responsive-host/child.component.ts | 14 + .../responsive-host/container.component.ts | 26 ++ .../responsive-host/example.component.html | 33 ++ .../responsive-host/example.component.scss | 5 + .../responsive-host/example.component.spec.ts | 61 +++ .../responsive-host/example.component.ts | 26 ++ .../core/numeric/basic/example.component.html | 18 + .../numeric/basic/example.component.spec.ts | 65 ++++ .../core/numeric/basic/example.component.ts | 23 ++ .../data-manager/data-manager/basic/data.ts | 57 +++ .../data-manager/basic/example.component.html | 5 + .../data-manager/basic/example.component.ts | 68 ++++ .../basic/filter-modal.component.html | 31 ++ .../basic/filter-modal.component.ts | 59 +++ .../data-manager/basic/filters.ts | 4 + .../basic/view-grid.component.html | 12 + .../data-manager/basic/view-grid.component.ts | 260 +++++++++++++ .../basic/view-repeater.component.html | 20 + .../basic/view-repeater.component.ts | 218 +++++++++++ .../date-pipe/basic/example.component.html | 3 + .../date-pipe/basic/example.component.ts | 11 + .../basic/example.component.html | 24 ++ .../basic/example.component.spec.ts | 63 ++++ .../basic/example.component.ts | 71 ++++ .../custom-calculator/example.component.html | 28 ++ .../custom-calculator/example.component.ts | 108 ++++++ .../help-key/example.component.html | 12 + .../help-key/example.component.spec.ts | 64 ++++ .../help-key/example.component.ts | 41 ++ .../datepicker/basic/example.component.html | 24 ++ .../basic/example.component.spec.ts | 74 ++++ .../datepicker/basic/example.component.ts | 64 ++++ .../custom-dates/example.component.html | 15 + .../custom-dates/example.component.ts | 124 ++++++ .../datepicker/fuzzy/example.component.html | 23 ++ .../fuzzy/example.component.spec.ts | 60 +++ .../datepicker/fuzzy/example.component.ts | 41 ++ .../timepicker/basic/example.component.html | 23 ++ .../timepicker/basic/example.component.ts | 62 +++ .../error/embedded/example.component.html | 61 +++ .../error/embedded/example.component.ts | 13 + .../errors/error/modal/example.component.html | 7 + .../errors/error/modal/example.component.ts | 19 + .../flyout/basic/example.component.html | 23 ++ .../flyout/flyout/basic/example.component.ts | 48 +++ .../flyout/flyout/basic/flyout.component.ts | 21 ++ .../custom-headers/example.component.html | 23 ++ .../custom-headers/example.component.ts | 73 ++++ .../flyout/custom-headers/flyout.component.ts | 21 ++ .../character-count/example.component.html | 33 ++ .../character-count/example.component.spec.ts | 75 ++++ .../character-count/example.component.ts | 41 ++ .../checkbox/basic/example.component.html | 32 ++ .../checkbox/basic/example.component.spec.ts | 119 ++++++ .../forms/checkbox/basic/example.component.ts | 57 +++ .../checkbox/help-key/example.component.html | 33 ++ .../help-key/example.component.spec.ts | 153 ++++++++ .../checkbox/help-key/example.component.ts | 57 +++ .../icon-group/example.component.html | 30 ++ .../checkbox/icon-group/example.component.ts | 30 ++ .../field-group/basic/example.component.html | 48 +++ .../field-group/basic/example.component.ts | 91 +++++ .../help-key/example.component.html | 48 +++ .../help-key/example.component.spec.ts | 40 ++ .../field-group/help-key/example.component.ts | 88 +++++ .../basic/example.component.html | 31 ++ .../basic/example.component.spec.ts | 75 ++++ .../basic/example.component.ts | 64 ++++ .../help-key/example.component.html | 18 + .../help-key/example.component.ts | 64 ++++ .../file-drop/basic/example.component.html | 26 ++ .../file-drop/basic/example.component.spec.ts | 113 ++++++ .../file-drop/basic/example.component.ts | 79 ++++ .../file-drop/help-key/example.component.html | 19 + .../file-drop/help-key/example.component.ts | 63 ++++ .../input-box/basic/example.component.html | 99 +++++ .../input-box/basic/example.component.spec.ts | 151 ++++++++ .../input-box/basic/example.component.ts | 61 +++ .../radio/help-key/example.component.html | 28 ++ .../radio/help-key/example.component.spec.ts | 107 ++++++ .../forms/radio/help-key/example.component.ts | 71 ++++ .../forms/radio/icon/example.component.html | 19 + .../forms/radio/icon/example.component.ts | 35 ++ .../radio/standard/example.component.html | 29 ++ .../radio/standard/example.component.spec.ts | 91 +++++ .../forms/radio/standard/example.component.ts | 75 ++++ .../checkbox/example.component.html | 20 + .../checkbox/example.component.ts | 74 ++++ .../radio/example.component.html | 22 ++ .../selection-box/radio/example.component.ts | 55 +++ .../basic/example.component.html | 7 + .../toggle-switch/basic/example.component.ts | 30 ++ .../help-key/example.component.html | 7 + .../help-key/example.component.ts | 28 ++ .../help-inline/basic/example.component.html | 8 + .../basic/example.component.spec.ts | 37 ++ .../help-inline/basic/example.component.ts | 13 + .../modules/icon/basic/example.component.html | 27 ++ .../icon/basic/example.component.spec.ts | 41 ++ .../modules/icon/basic/example.component.ts | 9 + .../icon/icon-button/example.component.html | 16 + .../icon-button/example.component.spec.ts | 48 +++ .../icon/icon-button/example.component.ts | 17 + .../alert/basic/example.component.html | 9 + .../alert/basic/example.component.spec.ts | 56 +++ .../alert/basic/example.component.ts | 17 + .../illustration/basic/example.component.html | 5 + .../basic/example.component.spec.ts | 40 ++ .../illustration/basic/example.component.ts | 22 ++ .../illustration-demo-resolver.service.ts | 14 + .../key-info/basic/example.component.html | 14 + .../key-info/basic/example.component.spec.ts | 72 ++++ .../key-info/basic/example.component.ts | 25 ++ .../key-info/help-key/example.component.html | 10 + .../help-key/example.component.spec.ts | 75 ++++ .../key-info/help-key/example.component.ts | 25 ++ .../label/basic/example.component.html | 7 + .../label/basic/example.component.spec.ts | 85 +++++ .../label/basic/example.component.ts | 60 +++ .../basic/example.component.html | 51 +++ .../basic/example.component.spec.ts | 98 +++++ .../basic/example.component.ts | 9 + .../help-key/example.component.html | 49 +++ .../help-key/example.component.spec.ts | 119 ++++++ .../help-key/example.component.ts | 9 + .../basic/example.component.html | 19 + .../text-highlight/basic/example.component.ts | 19 + .../tokens/basic/example.component.html | 1 + .../tokens/basic/example.component.spec.ts | 65 ++++ .../tokens/basic/example.component.ts | 25 ++ .../tokens/custom/example.component.html | 57 +++ .../tokens/custom/example.component.spec.ts | 120 ++++++ .../tokens/custom/example.component.ts | 86 +++++ .../wait/element/example.component.html | 12 + .../wait/element/example.component.spec.ts | 42 +++ .../wait/element/example.component.ts | 11 + .../wait/page/example.component.html | 15 + .../wait/page/example.component.spec.ts | 58 +++ .../indicators/wait/page/example.component.ts | 33 ++ .../inline-form/basic/example.component.html | 33 ++ .../inline-form/basic/example.component.ts | 66 ++++ .../custom-buttons/example.component.html | 33 ++ .../custom-buttons/example.component.ts | 110 ++++++ .../repeaters/example.component.html | 41 ++ .../repeaters/example.component.ts | 108 ++++++ .../basic/example.component.html | 17 + .../action-button/basic/example.component.ts | 17 + .../permalink/example.component.html | 13 + .../permalink/example.component.ts | 24 ++ .../infinite-scroll/example.component.html | 13 + .../infinite-scroll/example.component.ts | 164 ++++++++ .../back-to-top/infinite-scroll/person.ts | 4 + .../repeater/example.component.html | 12 + .../back-to-top/repeater/example.component.ts | 117 ++++++ .../layout/box/basic/example.component.html | 21 ++ .../box/basic/example.component.spec.ts | 36 ++ .../layout/box/basic/example.component.ts | 10 + .../box/help-key/example.component.html | 17 + .../box/help-key/example.component.spec.ts | 51 +++ .../layout/box/help-key/example.component.ts | 10 + .../layout/card/basic/example.component.html | 62 +++ .../layout/card/basic/example.component.ts | 21 ++ .../basic/example.component.html | 15 + .../basic/example.component.ts | 28 ++ .../help-key/example.component.html | 12 + .../help-key/example.component.ts | 29 ++ .../horizontal/example.component.html | 12 + .../horizontal/example.component.ts | 30 ++ .../inline-help/example.component.html | 17 + .../inline-help/example.component.ts | 35 ++ .../long-description/example.component.html | 12 + .../long-description/example.component.ts | 26 ++ .../vertical/example.component.html | 12 + .../vertical/example.component.ts | 30 ++ .../layout/fluid-grid/example.component.html | 104 ++++++ .../fluid-grid/example.component.spec.ts | 128 +++++++ .../layout/fluid-grid/example.component.ts | 21 ++ .../layout/format/example.component.html | 12 + .../layout/format/example.component.scss | 7 + .../layout/format/example.component.ts | 10 + .../custom/example.component.html | 19 + .../custom/example.component.scss | 15 + .../inline-delete/custom/example.component.ts | 35 ++ .../repeater/example.component.html | 38 ++ .../repeater/example.component.ts | 65 ++++ .../page-summary/basic/example.component.html | 74 ++++ .../page-summary/basic/example.component.ts | 34 ++ .../page/layout-fit/example.component.html | 33 ++ .../page/layout-fit/example.component.scss | 49 +++ .../page/layout-fit/example.component.ts | 10 + .../example.component.html | 1 + .../text-expand-repeater/example.component.ts | 17 + .../text-expand/inline/example.component.html | 1 + .../inline/example.component.spec.ts | 57 +++ .../text-expand/inline/example.component.ts | 12 + .../text-expand/modal/example.component.html | 1 + .../modal/example.component.spec.ts | 62 +++ .../text-expand/modal/example.component.ts | 12 + .../newline/example.component.html | 1 + .../text-expand/newline/example.component.ts | 12 + .../toolbar/basic/example.component.html | 38 ++ .../toolbar/basic/example.component.spec.ts | 52 +++ .../layout/toolbar/basic/example.component.ts | 14 + .../toolbar/sectioned/example.component.html | 60 +++ .../sectioned/example.component.spec.ts | 58 +++ .../toolbar/sectioned/example.component.ts | 14 + .../filter/inline/example.component.html | 44 +++ .../lists/filter/inline/example.component.ts | 150 ++++++++ .../lists/filter/modal/example.component.html | 30 ++ .../lists/filter/modal/example.component.ts | 132 +++++++ .../filter/modal/filter-modal-context.ts | 5 + .../filter/modal/filter-modal.component.html | 31 ++ .../filter/modal/filter-modal.component.ts | 76 ++++ .../lib/modules/lists/filter/modal/filter.ts | 5 + .../lib/modules/lists/filter/modal/fruit.ts | 5 + .../repeater/example.component.html | 12 + .../repeater/example.component.ts | 55 +++ .../lists/infinite-scroll/repeater/item.ts | 3 + .../lists/paging/basic/example.component.html | 8 + .../lists/paging/basic/example.component.ts | 11 + .../paging/with-content/demo-data.service.ts | 108 ++++++ .../lists/paging/with-content/demo-data.ts | 6 + .../with-content/example.component.html | 31 ++ .../paging/with-content/example.component.ts | 42 +++ .../lists/paging/with-content/person.ts | 4 + .../add-remove/example.component.html | 62 +++ .../add-remove/example.component.scss | 8 + .../add-remove/example.component.spec.ts | 143 +++++++ .../repeater/add-remove/example.component.ts | 63 ++++ .../modules/lists/repeater/add-remove/item.ts | 6 + .../repeater/basic/example.component.html | 47 +++ .../repeater/basic/example.component.scss | 8 + .../repeater/basic/example.component.spec.ts | 74 ++++ .../lists/repeater/basic/example.component.ts | 47 +++ .../inline-form/example.component.html | 41 ++ .../repeater/inline-form/example.component.ts | 107 ++++++ .../lists/sort/basic/example.component.html | 32 ++ .../lists/sort/basic/example.component.ts | 117 ++++++ .../advanced/example.component.html | 44 +++ .../advanced/example.component.ts | 72 ++++ .../lookup/autocomplete/advanced/planet.ts | 4 + .../autocomplete/basic/example.component.html | 18 + .../basic/example.component.spec.ts | 59 +++ .../autocomplete/basic/example.component.ts | 41 ++ .../custom-search/example.component.html | 32 ++ .../custom-search/example.component.ts | 74 ++++ .../autocomplete/custom-search/ocean.ts | 4 + .../search-filters/example.component.html | 22 ++ .../search-filters/example.component.ts | 54 +++ .../basic/example.component.html | 16 + .../basic/example.component.spec.ts | 58 +++ .../country-field/basic/example.component.ts | 60 +++ .../add-item/add-item-modal.component.html | 23 ++ .../add-item/add-item-modal.component.ts | 47 +++ .../lookup/add-item/example.component.html | 31 ++ .../lookup/add-item/example.component.spec.ts | 114 ++++++ .../lookup/add-item/example.component.ts | 117 ++++++ .../lookup/lookup/add-item/example.service.ts | 61 +++ .../modules/lookup/lookup/add-item/person.ts | 4 + .../lookup/lookup/add-item/search-results.ts | 7 + .../custom-picker/example.component.html | 30 ++ .../custom-picker/example.component.spec.ts | 205 ++++++++++ .../lookup/custom-picker/example.component.ts | 124 ++++++ .../lookup/custom-picker/example.service.ts | 121 ++++++ .../lookup/lookup/custom-picker/person.ts | 4 + .../lookup/custom-picker/picker-harness.ts | 21 ++ .../custom-picker/picker-modal.component.html | 39 ++ .../custom-picker/picker-modal.component.scss | 3 + .../custom-picker/picker-modal.component.ts | 81 ++++ .../lookup/custom-picker/search-results.ts | 7 + .../multi-select/example.component.html | 37 ++ .../multi-select/example.component.spec.ts | 110 ++++++ .../lookup/multi-select/example.component.ts | 109 ++++++ .../lookup/multi-select/example.service.ts | 61 +++ .../lookup/lookup/multi-select/person.ts | 3 + .../lookup/multi-select/search-results.ts | 7 + .../result-templates/example.component.html | 49 +++ .../example.component.spec.ts | 174 +++++++++ .../result-templates/example.component.ts | 115 ++++++ .../result-templates/example.service.ts | 121 ++++++ .../lookup/result-templates/item-harness.ts | 20 + .../lookup/lookup/result-templates/person.ts | 4 + .../lookup/result-templates/search-results.ts | 7 + .../single-select/example.component.html | 38 ++ .../single-select/example.component.spec.ts | 108 ++++++ .../lookup/single-select/example.component.ts | 109 ++++++ .../lookup/single-select/example.service.ts | 61 +++ .../lookup/lookup/single-select/person.ts | 3 + .../lookup/single-select/search-results.ts | 7 + .../search/basic/example.component.html | 36 ++ .../search/basic/example.component.spec.ts | 56 +++ .../lookup/search/basic/example.component.ts | 72 ++++ .../lib/modules/lookup/search/basic/item.ts | 4 + .../add-item/add-item-modal.component.html | 21 ++ .../add-item/add-item-modal.component.ts | 41 ++ .../add-item/example.component.html | 22 ++ .../add-item/example.component.spec.ts | 109 ++++++ .../add-item/example.component.ts | 75 ++++ .../add-item/example.service.ts | 56 +++ .../lookup/selection-modal/add-item/person.ts | 4 + .../add-item/search-results.ts | 7 + .../basic/example.component.html | 22 ++ .../basic/example.component.spec.ts | 108 ++++++ .../basic/example.component.ts | 47 +++ .../selection-modal/basic/example.service.ts | 52 +++ .../lookup/selection-modal/basic/person.ts | 4 + .../selection-modal/basic/search-results.ts | 7 + .../example.component.spec.ts | 62 +++ .../example.component.ts | 30 ++ .../basic-with-harness/example.component.html | 49 +++ .../example.component.spec.ts | 75 ++++ .../basic-with-harness/example.component.ts | 106 ++++++ .../example.component.spec.ts | 46 +++ .../example.component.ts | 71 ++++ .../basic-with-controller/help.service.ts | 30 ++ .../basic-with-controller/modal-context.ts | 10 + .../basic-with-controller/modal.component.ts | 64 ++++ .../basic-with-harness-help-key/context.ts | 5 + .../data.service.ts | 27 ++ .../modal/basic-with-harness-help-key/data.ts | 3 + .../example.component.html | 10 + .../example.component.spec.ts | 69 ++++ .../example.component.ts | 62 +++ .../modal.component.html | 20 + .../modal.component.ts | 45 +++ .../modal/basic-with-harness/context.ts | 5 + .../modal/basic-with-harness/data.service.ts | 27 ++ .../modals/modal/basic-with-harness/data.ts | 3 + .../basic-with-harness/example.component.html | 10 + .../example.component.spec.ts | 73 ++++ .../basic-with-harness/example.component.ts | 60 +++ .../basic-with-harness/modal.component.html | 20 + .../basic-with-harness/modal.component.ts | 45 +++ .../modals/modal/with-error/context.ts | 5 + .../modals/modal/with-error/data.service.ts | 29 ++ .../modules/modals/modal/with-error/data.ts | 3 + .../modal/with-error/example.component.html | 10 + .../with-error/example.component.spec.ts | 60 +++ .../modal/with-error/example.component.ts | 60 +++ .../modal/with-error/modal.component.html | 27 ++ .../modal/with-error/modal.component.ts | 57 +++ .../navbar/navbar/example.component.html | 23 ++ .../navbar/navbar/example.component.ts | 15 + .../pages/action-hub/example.component.html | 25 ++ .../action-hub/example.component.spec.ts | 110 ++++++ .../pages/action-hub/example.component.ts | 202 ++++++++++ .../pages/action-hub/modal-title-token.ts | 3 + .../action-hub/settings-modal.component.html | 41 ++ .../settings-modal.component.spec.ts | 70 ++++ .../action-hub/settings-modal.component.ts | 51 +++ .../example.component.html | 27 ++ .../example.component.spec.ts | 55 +++ .../example.component.ts | 25 ++ .../home-page-content.component.html | 1 + .../home-page-content.component.ts | 58 +++ .../tile-my-actions.component.html | 54 +++ .../tile-my-actions.component.ts | 39 ++ .../tile-updates.component.html | 28 ++ .../tile-updates.component.ts | 14 + ...ashboards-grid-context-menu.component.html | 22 ++ .../dashboards-grid-context-menu.component.ts | 26 ++ .../example.component.html | 6 + .../example.component.spec.ts | 60 +++ .../example.component.ts | 11 + .../page/list-page-list-layout-demo/item.ts | 5 + .../list-page-content.component.html | 29 ++ .../list-page-content.component.ts | 133 +++++++ .../contact-context-menu.component.html | 17 + .../contact-context-menu.component.ts | 24 ++ .../list-page-tabs-layout-demo/contact.ts | 5 + .../example.component.html | 6 + .../example.component.spec.ts | 62 +++ .../example.component.ts | 11 + .../list-page-contacts-grid.component.html | 29 ++ .../list-page-contacts-grid.component.ts | 115 ++++++ .../list-page-content.component.html | 12 + .../list-page-content.component.ts | 76 ++++ .../example.component.html | 30 ++ .../example.component.spec.ts | 68 ++++ .../example.component.ts | 24 ++ .../record-page-content.component.html | 93 +++++ .../record-page-content.component.scss | 13 + .../record-page-content.component.ts | 98 +++++ .../attachment.ts | 6 + ...tachments-grid-context-menu.component.html | 17 + ...attachments-grid-context-menu.component.ts | 26 ++ .../record-page-tabs-layout-demo/detail.ts | 4 + .../example.component.html | 28 ++ .../example.component.spec.ts | 64 ++++ .../example.component.ts | 19 + ...record-page-attachments-tab.component.html | 29 ++ .../record-page-attachments-tab.component.ts | 120 ++++++ .../record-page-content.component.html | 13 + .../record-page-content.component.ts | 20 + .../record-page-notes-tab.component.html | 92 +++++ .../record-page-notes-tab.component.ts | 36 ++ .../record-page-overview-tab.component.html | 93 +++++ .../record-page-overview-tab.component.scss | 13 + .../record-page-overview-tab.component.ts | 100 +++++ .../example.component.html | 25 ++ .../example.component.spec.ts | 54 +++ .../example.component.ts | 12 + .../split-view-page-content.component.html | 84 +++++ .../split-view-page-content.component.ts | 219 +++++++++++ .../phone-field/basic/example.component.html | 23 ++ .../basic/example.component.spec.ts | 107 ++++++ .../phone-field/basic/example.component.ts | 37 ++ .../dropdown/basic/example.component.html | 32 ++ .../dropdown/basic/example.component.spec.ts | 82 ++++ .../dropdown/basic/example.component.ts | 26 ++ .../popover/basic/example.component.html | 14 + .../popover/basic/example.component.spec.ts | 83 +++++ .../popover/basic/example.component.ts | 18 + .../programmatic/example.component.html | 21 ++ .../popover/programmatic/example.component.ts | 35 ++ .../basic/example.component.html | 18 + .../basic/example.component.spec.ts | 40 ++ .../basic/example.component.ts | 9 + .../basic/example.component.html | 113 ++++++ .../basic/example.component.spec.ts | 57 +++ .../basic/example.component.ts | 98 +++++ .../basic/modal-context.ts | 4 + .../basic/modal.component.html | 11 + .../basic/modal.component.ts | 18 + .../wizard/basic/example.component.html | 3 + .../wizard/basic/example.component.ts | 18 + .../wizard/basic/modal.component.html | 54 +++ .../wizard/basic/modal.component.ts | 61 +++ .../custom-sky-href-resolver.service.spec.ts | 64 ++++ .../custom-sky-href-resolver.service.ts | 60 +++ .../router/href/basic/example.component.css | 16 + .../router/href/basic/example.component.html | 25 ++ .../href/basic/example.component.spec.ts | 83 +++++ .../router/href/basic/example.component.ts | 11 + .../split-view/basic/example.component.html | 77 ++++ .../split-view/basic/example.component.ts | 188 ++++++++++ .../split-view/split-view/basic/record.ts | 9 + .../page-bound/example.component.html | 106 ++++++ .../page-bound/example.component.scss | 20 + .../page-bound/example.component.spec.ts | 129 +++++++ .../page-bound/example.component.ts | 249 +++++++++++++ .../split-view/page-bound/record.ts | 9 + .../modal/address-form.component.ts | 9 + .../modal/example.component.html | 3 + .../sectioned-form/modal/example.component.ts | 28 ++ .../modal/information-form.component.html | 26 ++ .../modal/information-form.component.ts | 81 ++++ .../sectioned-form/modal/modal.component.html | 45 +++ .../sectioned-form/modal/modal.component.ts | 51 +++ .../modal/phone-form.component.ts | 9 + .../dynamic-add-close/example.component.html | 7 + .../dynamic-add-close/example.component.ts | 39 ++ .../tabs/tabs/dynamic/example.component.html | 7 + .../tabs/tabs/dynamic/example.component.ts | 24 ++ .../static-add-close/example.component.html | 9 + .../static-add-close/example.component.ts | 15 + .../tabs/tabs/static/example.component.html | 5 + .../tabs/tabs/static/example.component.ts | 9 + .../basic/example.component.html | 7 + .../vertical-tabs/basic/example.component.ts | 9 + .../grouped/example.component.html | 20 + .../grouped/example.component.ts | 50 +++ .../tabs/vertical-tabs/grouped/group.ts | 12 + .../tabs/wizard/basic/example.component.html | 3 + .../tabs/wizard/basic/example.component.ts | 19 + .../tabs/wizard/basic/modal.component.html | 90 +++++ .../tabs/wizard/basic/modal.component.ts | 78 ++++ .../help-key/example.component.html | 17 + .../text-editor/help-key/example.component.ts | 41 ++ .../rich-text-display/example.component.html | 1 + .../rich-text-display/example.component.ts | 11 + .../text-editor/example.component.html | 20 + .../text-editor/example.component.ts | 41 ++ .../theme/box/basic/example.component.ts | 18 + .../basic/example.component.ts | 13 + .../tiles/tiles/basic/example.component.html | 4 + .../tiles/basic/example.component.spec.ts | 81 ++++ .../tiles/tiles/basic/example.component.ts | 57 +++ .../tiles/tiles/basic/tile1.component.html | 17 + .../tiles/tiles/basic/tile1.component.ts | 14 + .../tiles/tiles/basic/tile2.component.html | 8 + .../tiles/tiles/basic/tile2.component.ts | 10 + .../toast/toast/basic/example.component.html | 10 + .../toast/toast/basic/example.component.ts | 21 ++ .../toast/custom-component/custom-context.ts | 3 + .../custom-toast.component.html | 1 + .../custom-toast.component.ts | 18 + .../custom-component/example.component.html | 10 + .../custom-component/example.component.ts | 37 ++ .../control-validator/example.component.html | 7 + .../control-validator/example.component.ts | 34 ++ .../directive/example.component.html | 13 + .../directive/example.component.ts | 20 + .../control-validator/example.component.html | 7 + .../control-validator/example.component.ts | 36 ++ .../directive/example.component.html | 11 + .../directive/example.component.ts | 27 ++ libs/components/code-examples/tsconfig.json | 16 + .../code-examples/tsconfig.lib.json | 11 + .../code-examples/tsconfig.lib.prod.json | 9 + .../code-examples/tsconfig.spec.json | 8 + libs/components/packages/package.json | 1 + libs/components/tiles/project.json | 2 +- tsconfig.base.json | 1 + 640 files changed, 29273 insertions(+), 11 deletions(-) create mode 100644 libs/components/code-examples/README.md create mode 100644 libs/components/code-examples/eslint.config.js create mode 100644 libs/components/code-examples/karma.conf.js create mode 100644 libs/components/code-examples/ng-package.json create mode 100644 libs/components/code-examples/package.json create mode 100644 libs/components/code-examples/project.json create mode 100644 libs/components/code-examples/src/index.ts create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal-context.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal-context.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filters.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal-context.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filters.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filters.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/options-provider.ts create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/avatar/avatar/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/id/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/core/id/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/child.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/container.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filters.ts create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.html create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.html create mode 100644 libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/flyout/flyout/basic/flyout.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/flyout.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/character-count/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/character-count/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/character-count/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/icon/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/icon/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/icon/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/illustration/basic/illustration-demo-resolver.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/format/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/format/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/layout/format/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal-context.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/filter.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/filter/modal/fruit.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/item.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/paging/with-content/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/item.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/planet.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/ocean.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-harness.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/item-harness.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/search/basic/item.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/person.ts create mode 100644 libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/search-results.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/help.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal-context.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/context.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/context.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/context.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/modal-title-token.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/item.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachment.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/detail.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.html create mode 100644 libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.ts create mode 100755 libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal-context.ts create mode 100755 libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.html create mode 100755 libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts create mode 100644 libs/components/code-examples/src/lib/modules/router/href/basic/example.component.css create mode 100644 libs/components/code-examples/src/lib/modules/router/href/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/router/href/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/router/href/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/basic/record.ts create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.scss create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/record.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/address-form.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/phone-form.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/group.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/theme/box/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/theme/status-indicator/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.spec.ts create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.html create mode 100644 libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-context.ts create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.html create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.ts create mode 100644 libs/components/code-examples/tsconfig.json create mode 100644 libs/components/code-examples/tsconfig.lib.json create mode 100644 libs/components/code-examples/tsconfig.lib.prod.json create mode 100644 libs/components/code-examples/tsconfig.spec.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c03df95d84..69cde6976b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -171,7 +171,7 @@ jobs: fail-fast: false matrix: app: - - code-examples + - code-examples-playground - integration - playground # - dep-graph @@ -265,7 +265,7 @@ jobs: shell: bash run: | mkdir -p ./dist/apps - for app in code-examples dep-graph integration playground + for app in code-examples-playground dep-graph integration playground do if [ -d "./dist/${{ fromJson(needs.install-deps.outputs.parameters).storybooksPath }}${app}" ] then diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 35c30ee768..08d65490da 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -38,7 +38,7 @@ jobs: migrations packaging release - code-examples + apps/code-examples-playground apps/integration apps/playground components/a11y @@ -49,6 +49,7 @@ jobs: components/assets components/autonumeric components/avatar + components/code-examples components/colorpicker components/config components/core diff --git a/.skyuxdev.json b/.skyuxdev.json index 2150baf3c0..8bacf3dc1b 100644 --- a/.skyuxdev.json +++ b/.skyuxdev.json @@ -3,6 +3,7 @@ "documentationExcludeProjects": [ "animations", "assets", + "code-examples", "config", "e2e-schematics", "eslint-config", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 90222d5ea8..1022cceee6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -27,7 +27,7 @@ { "label": "Start code examples", "type": "shell", - "command": "npx nx serve code-examples", + "command": "npx nx serve code-examples-playground", "isBackground": true, "problemMatcher": { "owner": "typescript", @@ -42,7 +42,7 @@ }, "background": { "activeOnStart": true, - "beginsPattern": "> nx run code-examples:serve:development", + "beginsPattern": "> nx run code-examples-playground:serve:development", "endsPattern": "√ Compiled successfully." } } diff --git a/apps/code-examples/project.json b/apps/code-examples/project.json index c6552dce90..ae46587e3f 100644 --- a/apps/code-examples/project.json +++ b/apps/code-examples/project.json @@ -1,5 +1,5 @@ { - "name": "code-examples", + "name": "code-examples-playground", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "sourceRoot": "apps/code-examples/src", @@ -10,7 +10,7 @@ "outputs": ["{options.outputPath}"], "options": { "allowedCommonJsDependencies": ["dragula", "ng2-dragula"], - "outputPath": "dist/apps/code-examples", + "outputPath": "dist/apps/code-examples-playground", "index": "apps/code-examples/src/index.html", "main": "apps/code-examples/src/main.ts", "polyfills": ["zone.js", "libs/components/packages/src/polyfills.js"], @@ -64,10 +64,10 @@ "executor": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "buildTarget": "code-examples:build:production" + "buildTarget": "code-examples-playground:build:production" }, "development": { - "buildTarget": "code-examples:build:development" + "buildTarget": "code-examples-playground:build:development" } }, "defaultConfiguration": "development" @@ -75,7 +75,7 @@ "extract-i18n": { "executor": "@angular-devkit/build-angular:extract-i18n", "options": { - "buildTarget": "code-examples:build" + "buildTarget": "code-examples-playground:build" } }, "lint": { diff --git a/libs/components/code-examples/README.md b/libs/components/code-examples/README.md new file mode 100644 index 0000000000..97994e8bc4 --- /dev/null +++ b/libs/components/code-examples/README.md @@ -0,0 +1 @@ +# @skyux/code-examples (Internal SKY UX use only) diff --git a/libs/components/code-examples/eslint.config.js b/libs/components/code-examples/eslint.config.js new file mode 100644 index 0000000000..e5dfec200c --- /dev/null +++ b/libs/components/code-examples/eslint.config.js @@ -0,0 +1,41 @@ +const tsEslint = require('typescript-eslint'); +const config = require('../../../eslint-libs.config'); +const skyux = require('../../sdk/skyux-eslint/dev-transpiler.cjs'); + +module.exports = tsEslint.config( + ...config, + { + files: ['**/*.ts'], + extends: [...skyux.configs.tsAll], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'app', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'app', + style: 'kebab-case', + }, + ], + '@nx/enforce-module-boundaries': 'warn', + '@typescript-eslint/no-deprecated': 'warn', + 'no-alert': 'warn', + 'no-console': 'warn', + }, + }, + { + files: ['**/*.html'], + extends: [...skyux.configs.templateAll], + rules: { + 'skyux-eslint-template/no-deprecated-directives': 'warn', + 'skyux-eslint-template/no-legacy-icons': 'warn', + }, + }, +); diff --git a/libs/components/code-examples/karma.conf.js b/libs/components/code-examples/karma.conf.js new file mode 100644 index 0000000000..b0d7653b32 --- /dev/null +++ b/libs/components/code-examples/karma.conf.js @@ -0,0 +1,25 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const { join } = require('path'); +const getBaseKarmaConfig = require('../../../karma.conf'); + +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); + config.set({ + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join(__dirname, '../../../coverage/libs/components/code-examples'), + // TODO: remove these threshold overrides to meet 100% coverage! + check: { + global: { + statements: 0, + branches: 0, + functions: 0, + lines: 0, + }, + }, + }, + }); +}; diff --git a/libs/components/code-examples/ng-package.json b/libs/components/code-examples/ng-package.json new file mode 100644 index 0000000000..f7caaf7e83 --- /dev/null +++ b/libs/components/code-examples/ng-package.json @@ -0,0 +1,9 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/libs/components/code-examples", + "lib": { + "entryFile": "src/index.ts", + "styleIncludePaths": ["../../.."] + }, + "inlineStyleLanguage": "scss" +} diff --git a/libs/components/code-examples/package.json b/libs/components/code-examples/package.json new file mode 100644 index 0000000000..efe9427168 --- /dev/null +++ b/libs/components/code-examples/package.json @@ -0,0 +1,64 @@ +{ + "name": "@skyux/code-examples", + "version": "0.0.0-PLACEHOLDER", + "author": "Blackbaud, Inc.", + "description": "Internal SKY UX use only", + "keywords": [ + "blackbaud", + "skyux" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/blackbaud/skyux.git" + }, + "bugs": { + "url": "https://github.com/blackbaud/skyux/issues" + }, + "homepage": "https://github.com/blackbaud/skyux#readme", + "peerDependencies": { + "@angular/cdk": "^19.0.4", + "@angular/common": "^19.0.5", + "@angular/core": "^19.0.5", + "@angular/forms": "^19.0.5", + "@angular/router": "^19.0.5", + "@blackbaud/angular-tree-component": "^1.0.0", + "@skyux/action-bars": "0.0.0-PLACEHOLDER", + "@skyux/ag-grid": "0.0.0-PLACEHOLDER", + "@skyux/angular-tree-component": "0.0.0-PLACEHOLDER", + "@skyux/autonumeric": "0.0.0-PLACEHOLDER", + "@skyux/avatar": "0.0.0-PLACEHOLDER", + "@skyux/colorpicker": "0.0.0-PLACEHOLDER", + "@skyux/core": "0.0.0-PLACEHOLDER", + "@skyux/data-manager": "0.0.0-PLACEHOLDER", + "@skyux/datetime": "0.0.0-PLACEHOLDER", + "@skyux/errors": "0.0.0-PLACEHOLDER", + "@skyux/flyout": "0.0.0-PLACEHOLDER", + "@skyux/forms": "0.0.0-PLACEHOLDER", + "@skyux/help-inline": "0.0.0-PLACEHOLDER", + "@skyux/icon": "0.0.0-PLACEHOLDER", + "@skyux/indicators": "0.0.0-PLACEHOLDER", + "@skyux/inline-form": "0.0.0-PLACEHOLDER", + "@skyux/layout": "0.0.0-PLACEHOLDER", + "@skyux/lists": "0.0.0-PLACEHOLDER", + "@skyux/lookup": "0.0.0-PLACEHOLDER", + "@skyux/modals": "0.0.0-PLACEHOLDER", + "@skyux/navbar": "0.0.0-PLACEHOLDER", + "@skyux/pages": "0.0.0-PLACEHOLDER", + "@skyux/phone-field": "0.0.0-PLACEHOLDER", + "@skyux/popovers": "0.0.0-PLACEHOLDER", + "@skyux/progress-indicator": "0.0.0-PLACEHOLDER", + "@skyux/router": "0.0.0-PLACEHOLDER", + "@skyux/split-view": "0.0.0-PLACEHOLDER", + "@skyux/tabs": "0.0.0-PLACEHOLDER", + "@skyux/text-editor": "0.0.0-PLACEHOLDER", + "@skyux/tiles": "0.0.0-PLACEHOLDER", + "@skyux/toast": "0.0.0-PLACEHOLDER", + "@skyux/validation": "0.0.0-PLACEHOLDER", + "ag-grid-angular": "^32.3.3", + "ag-grid-community": "^32.3.3" + }, + "dependencies": { + "tslib": "^2.8.1" + } +} diff --git a/libs/components/code-examples/project.json b/libs/components/code-examples/project.json new file mode 100644 index 0000000000..52a8b396b4 --- /dev/null +++ b/libs/components/code-examples/project.json @@ -0,0 +1,66 @@ +{ + "name": "code-examples", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/components/code-examples/src", + "prefix": "sky", + "targets": { + "build": { + "executor": "@nx/angular:package", + "outputs": ["{workspaceRoot}/dist/libs/components/code-examples"], + "options": { + "project": "libs/components/code-examples/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/components/code-examples/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "libs/components/code-examples/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "libs/components/code-examples/tsconfig.spec.json", + "karmaConfig": "libs/components/code-examples/karma.conf.js", + "styles": [ + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss" + ], + "codeCoverage": true, + "codeCoverageExclude": ["**/fixtures/**"], + "polyfills": [ + "zone.js", + "zone.js/testing", + "libs/components/packages/src/polyfills.js" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + } + }, + "configurations": { + "ci": { + "browsers": "ChromeHeadlessNoSandbox", + "codeCoverage": true, + "progress": false, + "sourceMap": true, + "watch": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": [ + "{projectRoot}/src/**/*.ts", + "{projectRoot}/src/**/*.html" + ] + } + } + }, + "tags": ["component", "npm"] +} diff --git a/libs/components/code-examples/src/index.ts b/libs/components/code-examples/src/index.ts new file mode 100644 index 0000000000..23d7b77edd --- /dev/null +++ b/libs/components/code-examples/src/index.ts @@ -0,0 +1,169 @@ +export { AvatarExampleComponent } from './lib/modules/avatar/avatar/example.component'; +export { CoreIdExampleComponent } from './lib/modules/core/id/example.component'; +export { FormsCharacterCountExampleComponent } from './lib/modules/forms/character-count/example.component'; +export { HelpInlineBasicExampleComponent } from './lib/modules/help-inline/basic/example.component'; +export { IconBasicExampleComponent } from './lib/modules/icon/basic/example.component'; +export { IconIconButtonExampleComponent } from './lib/modules/icon/icon-button/example.component'; +export { LayoutFluidGridExampleComponent } from './lib/modules/layout/fluid-grid/example.component'; +export { LayoutFormatExampleComponent } from './lib/modules/layout/format/example.component'; +export { LayoutTextExpandRepeaterExampleComponent } from './lib/modules/layout/text-expand-repeater/example.component'; +export { NavbarExampleComponent } from './lib/modules/navbar/navbar/example.component'; +export { PagesActionHubExampleComponent } from './lib/modules/pages/action-hub/example.component'; +export { TextEditorHelpKeyExampleComponent } from './lib/modules/text-editor/help-key/example.component'; +export { TextEditorRichTextDisplayExampleComponent } from './lib/modules/text-editor/rich-text-display/example.component'; +export { TextEditorExampleComponent } from './lib/modules/text-editor/text-editor/example.component'; +export { AutonumericCurrencyExampleComponent } from './lib/modules/autonumeric/autonumeric/currency/example.component'; +export { AutonumericInternationalFormattingExampleComponent } from './lib/modules/autonumeric/autonumeric/international-formatting/example.component'; +export { AutonumericOptionsProviderExampleComponent } from './lib/modules/autonumeric/autonumeric/options-provider/example.component'; +export { AutonumericPresetExampleComponent } from './lib/modules/autonumeric/autonumeric/preset/example.component'; +export { ActionBarsSummaryActionBarBasicExampleComponent } from './lib/modules/action-bars/summary-action-bar/basic/example.component'; +export { ActionBarsSummaryActionBarModalExampleComponent } from './lib/modules/action-bars/summary-action-bar/modal/example.component'; +export { ActionBarsSummaryActionBarTabExampleComponent } from './lib/modules/action-bars/summary-action-bar/tab/example.component'; +export { AngularTreeComponentAngularTreeBasicExampleComponent } from './lib/modules/angular-tree-component/angular-tree/basic/example.component'; +export { AngularTreeComponentAngularTreeHelpKeyExampleComponent } from './lib/modules/angular-tree-component/angular-tree/help-key/example.component'; +export { AgGridDataEntryGridBasicExampleComponent } from './lib/modules/ag-grid/data-entry-grid/basic/example.component'; +export { AgGridDataEntryGridDataManagerAddedExampleComponent } from './lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component'; +export { AgGridDataEntryGridFocusExampleComponent } from './lib/modules/ag-grid/data-entry-grid/focus/example.component'; +export { AgGridDataGridBasicExampleComponent } from './lib/modules/ag-grid/data-grid/basic/example.component'; +export { AgGridDataEntryGridInlineHelpExampleComponent } from './lib/modules/ag-grid/data-entry-grid/inline-help/example.component'; +export { AgGridDataGridBasicMultiselectExampleComponent } from './lib/modules/ag-grid/data-grid/basic-multiselect/example.component'; +export { AgGridDataGridDataManagerExampleComponent } from './lib/modules/ag-grid/data-grid/data-manager/example.component'; +export { AgGridDataGridDataManagerMultiselectExampleComponent } from './lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component'; +export { AgGridDataGridInlineHelpExampleComponent } from './lib/modules/ag-grid/data-grid/inline-help/example.component'; +export { AgGridDataGridPagingExampleComponent } from './lib/modules/ag-grid/data-grid/paging/example.component'; +export { AgGridDataGridTemplateRefColumnExampleComponent } from './lib/modules/ag-grid/data-grid/template-ref-column/example.component'; +export { AgGridDataGridTopScrollExampleComponent } from './lib/modules/ag-grid/data-grid/top-scroll/example.component'; +export { ColorpickerBasicExampleComponent } from './lib/modules/colorpicker/colorpicker/basic/example.component'; +export { ColorpickerHelpKeyExampleComponent } from './lib/modules/colorpicker/colorpicker/help-key/example.component'; +export { ColorpickerProgrammaticExampleComponent } from './lib/modules/colorpicker/colorpicker/programmatic/example.component'; +export { CoreMediaQueryBasicExampleComponent } from './lib/modules/core/media-query/basic/example.component'; +export { CoreMediaQueryResponsiveHostExampleComponent } from './lib/modules/core/media-query/responsive-host/example.component'; +export { CoreNumericBasicExampleComponent } from './lib/modules/core/numeric/basic/example.component'; +export { DatetimeDateRangePickerBasicExampleComponent } from './lib/modules/datetime/date-range-picker/basic/example.component'; +export { DatetimeDateRangePickerCustomCalculatorExampleComponent } from './lib/modules/datetime/date-range-picker/custom-calculator/example.component'; +export { DatetimeDateRangePickerHelpKeyExampleComponent } from './lib/modules/datetime/date-range-picker/help-key/example.component'; +export { DataManagerBasicExampleComponent } from './lib/modules/data-manager/data-manager/basic/example.component'; +export { DatetimeDatepickerBasicExampleComponent } from './lib/modules/datetime/datepicker/basic/example.component'; +export { DatetimeDatePipeBasicExampleComponent } from './lib/modules/datetime/date-pipe/basic/example.component'; +export { DatetimeDatepickerFuzzyExampleComponent } from './lib/modules/datetime/datepicker/fuzzy/example.component'; +export { DatetimeDatepickerCustomDatesExampleComponent } from './lib/modules/datetime/datepicker/custom-dates/example.component'; +export { FlyoutBasicExampleComponent } from './lib/modules/flyout/flyout/basic/example.component'; +export { DatetimeTimepickerBasicExampleComponent } from './lib/modules/datetime/timepicker/basic/example.component'; +export { FlyoutCustomHeadersExampleComponent } from './lib/modules/flyout/flyout/custom-headers/example.component'; +export { ErrorsErrorEmbeddedExampleComponent } from './lib/modules/errors/error/embedded/example.component'; +export { FormsCheckboxBasicExampleComponent } from './lib/modules/forms/checkbox/basic/example.component'; +export { ErrorsErrorModalExampleComponent } from './lib/modules/errors/error/modal/example.component'; +export { FormsCheckboxHelpKeyExampleComponent } from './lib/modules/forms/checkbox/help-key/example.component'; +export { FormsFieldGroupHelpKeyExampleComponent } from './lib/modules/forms/field-group/help-key/example.component'; +export { FormsCheckboxIconGroupExampleComponent } from './lib/modules/forms/checkbox/icon-group/example.component'; +export { FormsFileAttachmentBasicExampleComponent } from './lib/modules/forms/file-attachment/basic/example.component'; +export { FormsFieldGroupBasicExampleComponent } from './lib/modules/forms/field-group/basic/example.component'; +export { FormsFileDropBasicExampleComponent } from './lib/modules/forms/file-drop/basic/example.component'; +export { FormsFileAttachmentHelpKeyExampleComponent } from './lib/modules/forms/file-attachment/help-key/example.component'; +export { FormsFileDropHelpKeyExampleComponent } from './lib/modules/forms/file-drop/help-key/example.component'; +export { FormsInputBoxBasicExampleComponent } from './lib/modules/forms/input-box/basic/example.component'; +export { FormsRadioHelpKeyExampleComponent } from './lib/modules/forms/radio/help-key/example.component'; +export { FormsRadioIconExampleComponent } from './lib/modules/forms/radio/icon/example.component'; +export { FormsRadioStandardExampleComponent } from './lib/modules/forms/radio/standard/example.component'; +export { FormsSelectionBoxRadioExampleComponent } from './lib/modules/forms/selection-box/radio/example.component'; +export { FormsSelectionBoxCheckboxExampleComponent } from './lib/modules/forms/selection-box/checkbox/example.component'; +export { FormsToggleSwitchBasicExampleComponent } from './lib/modules/forms/toggle-switch/basic/example.component'; +export { FormsToggleSwitchHelpKeyExampleComponent } from './lib/modules/forms/toggle-switch/help-key/example.component'; +export { IndicatorsAlertBasicExampleComponent } from './lib/modules/indicators/alert/basic/example.component'; +export { IndicatorsIllustrationBasicExampleComponent } from './lib/modules/indicators/illustration/basic/example.component'; +export { IndicatorsKeyInfoHelpKeyExampleComponent } from './lib/modules/indicators/key-info/help-key/example.component'; +export { IndicatorsLabelBasicExampleComponent } from './lib/modules/indicators/label/basic/example.component'; +export { IndicatorsKeyInfoBasicExampleComponent } from './lib/modules/indicators/key-info/basic/example.component'; +export { IndicatorsStatusIndicatorBasicExampleComponent } from './lib/modules/indicators/status-indicator/basic/example.component'; +export { IndicatorsStatusIndicatorHelpKeyExampleComponent } from './lib/modules/indicators/status-indicator/help-key/example.component'; +export { IndicatorsTokensBasicExampleComponent } from './lib/modules/indicators/tokens/basic/example.component'; +export { IndicatorsTextHighlightBasicExampleComponent } from './lib/modules/indicators/text-highlight/basic/example.component'; +export { IndicatorsTokensCustomExampleComponent } from './lib/modules/indicators/tokens/custom/example.component'; +export { InlineFormRepeatersExampleComponent } from './lib/modules/inline-form/inline-form/repeaters/example.component'; +export { InlineFormBasicExampleComponent } from './lib/modules/inline-form/inline-form/basic/example.component'; +export { IndicatorsWaitElementExampleComponent } from './lib/modules/indicators/wait/element/example.component'; +export { InlineFormCustomButtonsExampleComponent } from './lib/modules/inline-form/inline-form/custom-buttons/example.component'; +export { LayoutBackToTopInfiniteScrollExampleComponent } from './lib/modules/layout/back-to-top/infinite-scroll/example.component'; +export { IndicatorsWaitPageExampleComponent } from './lib/modules/indicators/wait/page/example.component'; +export { LayoutActionButtonPermalinkExampleComponent } from './lib/modules/layout/action-button/permalink/example.component'; +export { LayoutActionButtonBasicExampleComponent } from './lib/modules/layout/action-button/basic/example.component'; +export { LayoutBackToTopRepeaterExampleComponent } from './lib/modules/layout/back-to-top/repeater/example.component'; +export { LayoutBoxBasicExampleComponent } from './lib/modules/layout/box/basic/example.component'; +export { LayoutCardBasicExampleComponent } from './lib/modules/layout/card/basic/example.component'; +export { LayoutBoxHelpKeyExampleComponent } from './lib/modules/layout/box/help-key/example.component'; +export { LayoutDescriptionListHelpKeyExampleComponent } from './lib/modules/layout/description-list/help-key/example.component'; +export { LayoutDefinitionListBasicExampleComponent } from './lib/modules/layout/definition-list/basic/example.component'; +export { LayoutDescriptionListInlineHelpExampleComponent } from './lib/modules/layout/description-list/inline-help/example.component'; +export { LayoutDescriptionListHorizontalExampleComponent } from './lib/modules/layout/description-list/horizontal/example.component'; +export { LayoutInlineDeleteCustomExampleComponent } from './lib/modules/layout/inline-delete/custom/example.component'; +export { LayoutDescriptionListLongDescriptionExampleComponent } from './lib/modules/layout/description-list/long-description/example.component'; +export { LayoutInlineDeleteRepeaterExampleComponent } from './lib/modules/layout/inline-delete/repeater/example.component'; +export { LayoutDescriptionListVerticalExampleComponent } from './lib/modules/layout/description-list/vertical/example.component'; +export { LayoutPageLayoutFitExampleComponent } from './lib/modules/layout/page/layout-fit/example.component'; +export { LayoutTextExpandInlineExampleComponent } from './lib/modules/layout/text-expand/inline/example.component'; +export { LayoutPageSummaryBasicExampleComponent } from './lib/modules/layout/page-summary/basic/example.component'; +export { LayoutTextExpandModalExampleComponent } from './lib/modules/layout/text-expand/modal/example.component'; +export { LayoutToolbarBasicExampleComponent } from './lib/modules/layout/toolbar/basic/example.component'; +export { LayoutTextExpandNewlineExampleComponent } from './lib/modules/layout/text-expand/newline/example.component'; +export { LayoutToolbarSectionedExampleComponent } from './lib/modules/layout/toolbar/sectioned/example.component'; +export { ListsFilterInlineExampleComponent } from './lib/modules/lists/filter/inline/example.component'; +export { ListsFilterModalExampleComponent } from './lib/modules/lists/filter/modal/example.component'; +export { ListsPagingWithContentExampleComponent } from './lib/modules/lists/paging/with-content/example.component'; +export { ListsPagingBasicExampleComponent } from './lib/modules/lists/paging/basic/example.component'; +export { ListsRepeaterAddRemoveExampleComponent } from './lib/modules/lists/repeater/add-remove/example.component'; +export { ListsInfiniteScrollRepeaterExampleComponent } from './lib/modules/lists/infinite-scroll/repeater/example.component'; +export { ListsRepeaterInlineFormExampleComponent } from './lib/modules/lists/repeater/inline-form/example.component'; +export { ListsRepeaterBasicExampleComponent } from './lib/modules/lists/repeater/basic/example.component'; +export { ListsSortBasicExampleComponent } from './lib/modules/lists/sort/basic/example.component'; +export { LookupAutocompleteBasicExampleComponent } from './lib/modules/lookup/autocomplete/basic/example.component'; +export { LookupAutocompleteCustomSearchExampleComponent } from './lib/modules/lookup/autocomplete/custom-search/example.component'; +export { LookupAutocompleteAdvancedExampleComponent } from './lib/modules/lookup/autocomplete/advanced/example.component'; +export { LookupAutocompleteSearchFiltersExampleComponent } from './lib/modules/lookup/autocomplete/search-filters/example.component'; +export { LookupCountryFieldBasicExampleComponent } from './lib/modules/lookup/country-field/basic/example.component'; +export { LookupCustomPickerExampleComponent } from './lib/modules/lookup/lookup/custom-picker/example.component'; +export { LookupAddItemExampleComponent } from './lib/modules/lookup/lookup/add-item/example.component'; +export { LookupMultiSelectExampleComponent } from './lib/modules/lookup/lookup/multi-select/example.component'; +export { LookupResultTemplatesExampleComponent } from './lib/modules/lookup/lookup/result-templates/example.component'; +export { LookupSingleSelectExampleComponent } from './lib/modules/lookup/lookup/single-select/example.component'; +export { LookupSearchBasicExampleComponent } from './lib/modules/lookup/search/basic/example.component'; +export { LookupSelectionModalAddItemExampleComponent } from './lib/modules/lookup/selection-modal/add-item/example.component'; +export { LookupSelectionModalBasicExampleComponent } from './lib/modules/lookup/selection-modal/basic/example.component'; +export { ModalsConfirmBasicWithControllerExampleComponent } from './lib/modules/modals/confirm/basic-with-controller/example.component'; +export { ModalsModalBasicWithControllerExampleComponent } from './lib/modules/modals/modal/basic-with-controller/example.component'; +export { ModalsConfirmBasicWithHarnessExampleComponent } from './lib/modules/modals/confirm/basic-with-harness/example.component'; +export { ModalsModalBasicWithHarnessExampleComponent } from './lib/modules/modals/modal/basic-with-harness/example.component'; +export { ModalsModalBasicWithHarnessHelpKeyExampleComponent } from './lib/modules/modals/modal/basic-with-harness-help-key/example.component'; +export { ModalsModalWithErrorExampleComponent } from './lib/modules/modals/modal/with-error/example.component'; +export { PagesPageHomePageBlocksLayoutExampleComponent } from './lib/modules/pages/page/home-page-blocks-layout-demo/example.component'; +export { PagesPageListPageTabsLayoutExampleComponent } from './lib/modules/pages/page/list-page-tabs-layout-demo/example.component'; +export { PagesPageListPageListLayoutExampleComponent } from './lib/modules/pages/page/list-page-list-layout-demo/example.component'; +export { PagesPageRecordPageBlocksLayoutExampleComponent } from './lib/modules/pages/page/record-page-blocks-layout-demo/example.component'; +export { PagesPageRecordPageTabsLayoutExampleComponent } from './lib/modules/pages/page/record-page-tabs-layout-demo/example.component'; +export { PagesPageSplitViewPageFitLayoutExampleComponent } from './lib/modules/pages/page/split-view-page-fit-layout-demo/example.component'; +export { PhoneFieldBasicExampleComponent } from './lib/modules/phone-field/phone-field/basic/example.component'; +export { PopoversDropdownBasicExampleComponent } from './lib/modules/popovers/dropdown/basic/example.component'; +export { PopoversPopoverBasicExampleComponent } from './lib/modules/popovers/popover/basic/example.component'; +export { PopoversPopoverProgrammaticExampleComponent } from './lib/modules/popovers/popover/programmatic/example.component'; +export { SplitViewBasicExampleComponent } from './lib/modules/split-view/split-view/basic/example.component'; +export { SplitViewPageBoundExampleComponent } from './lib/modules/split-view/split-view/page-bound/example.component'; +export { ProgressIndicatorPassiveIndicatorBasicExampleComponent } from './lib/modules/progress-indicator/passive-indicator/basic/example.component'; +export { ProgressIndicatorWizardBasicExampleComponent } from './lib/modules/progress-indicator/wizard/basic/example.component'; +export { RouterHrefBasicExampleComponent } from './lib/modules/router/href/basic/example.component'; +export { ProgressIndicatorWaterfallIndicatorBasicExampleComponent } from './lib/modules/progress-indicator/waterfall-indicator/basic/example.component'; +export { TabsSectionedFormModalExampleComponent } from './lib/modules/tabs/sectioned-form/modal/example.component'; +export { TabsDynamicExampleComponent } from './lib/modules/tabs/tabs/dynamic/example.component'; +export { TabsStaticExampleComponent } from './lib/modules/tabs/tabs/static/example.component'; +export { TabsStaticAddCloseExampleComponent } from './lib/modules/tabs/tabs/static-add-close/example.component'; +export { TabsDynamicAddCloseExampleComponent } from './lib/modules/tabs/tabs/dynamic-add-close/example.component'; +export { TabsVerticalTabsBasicExampleComponent } from './lib/modules/tabs/vertical-tabs/basic/example.component'; +export { TabsWizardBasicExampleComponent } from './lib/modules/tabs/wizard/basic/example.component'; +export { TabsVerticalTabsGroupedExampleComponent } from './lib/modules/tabs/vertical-tabs/grouped/example.component'; +export { ThemeBoxBasicExampleComponent } from './lib/modules/theme/box/basic/example.component'; +export { ThemeStatusIndicatorBasicExampleComponent } from './lib/modules/theme/status-indicator/basic/example.component'; +export { TilesBasicExampleComponent } from './lib/modules/tiles/tiles/basic/example.component'; +export { ToastCustomComponentExampleComponent } from './lib/modules/toast/toast/custom-component/example.component'; +export { ToastBasicExampleComponent } from './lib/modules/toast/toast/basic/example.component'; +export { ValidationEmailValidationDirectiveExampleComponent } from './lib/modules/validation/email-validation/directive/example.component'; +export { ValidationEmailValidationControlValidatorExampleComponent } from './lib/modules/validation/email-validation/control-validator/example.component'; +export { ValidationUrlValidationControlValidatorExampleComponent } from './lib/modules/validation/url-validation/control-validator/example.component'; +export { ValidationUrlValidationDirectiveExampleComponent } from './lib/modules/validation/url-validation/directive/example.component'; diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.html b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.html new file mode 100644 index 0000000000..b48e2be5d4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.html @@ -0,0 +1,35 @@ + + + + Primary action + + + + Secondary action + + + Secondary action 2 + + + + + + $250 + Given this month + + + $1,000 + Given this year + + + $1,300 + Given all time + + + diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.ts new file mode 100644 index 0000000000..10548636a8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/basic/example.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { SkySummaryActionBarModule } from '@skyux/action-bars'; +import { SkyKeyInfoModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-action-bars-summary-action-bar-basic-example', + templateUrl: './example.component.html', + imports: [SkyKeyInfoModule, SkySummaryActionBarModule], +}) +export class ActionBarsSummaryActionBarBasicExampleComponent { + protected onPrimaryActionClick(): void { + alert('Primary action button clicked.'); + } + + protected onSecondaryActionClick(): void { + alert('Secondary action button clicked.'); + } + + protected onSecondaryAction2Click(): void { + alert('Secondary action 2 button clicked.'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.html b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.html new file mode 100644 index 0000000000..21a4b71c5f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.html @@ -0,0 +1,3 @@ + diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.ts b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.ts new file mode 100644 index 0000000000..fd91ca48c0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/example.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from '@angular/core'; +import { SkyModalService } from '@skyux/modals'; + +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-action-bars-summary-action-bar-modal-example', + templateUrl: './example.component.html', +}) +export class ActionBarsSummaryActionBarModalExampleComponent { + readonly #modalSvc = inject(SkyModalService); + + protected openModal(): void { + this.#modalSvc.open(ModalComponent, { + size: 'large', + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.html b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.html new file mode 100644 index 0000000000..0ca8aa2e2f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.html @@ -0,0 +1,41 @@ + + Hello world! + + + + + Primary action + + + + Secondary action + + + Secondary action 2 + + + + Cancel + + + + + $250 + Given this month + + + $1,000 + Given this year + + + $1,300 + Given all time + + + + + diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.ts b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.ts new file mode 100644 index 0000000000..d99417835b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/modal/modal.component.ts @@ -0,0 +1,29 @@ +import { Component, inject } from '@angular/core'; +import { SkySummaryActionBarModule } from '@skyux/action-bars'; +import { SkyKeyInfoModule } from '@skyux/indicators'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [SkyKeyInfoModule, SkyModalModule, SkySummaryActionBarModule], +}) +export class ModalComponent { + readonly #modalInstance = inject(SkyModalInstance); + + protected onSecondaryActionClick(): void { + alert('Secondary action button clicked.'); + } + + protected onSecondaryAction2Click(): void { + alert('Secondary action 2 button clicked.'); + } + + protected cancel(): void { + this.#modalInstance.cancel(); + } + + protected save(): void { + this.#modalInstance.save(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.html b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.html new file mode 100644 index 0000000000..62f4778726 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.html @@ -0,0 +1,41 @@ + + Tab without summary action bar. + + Tab with summary action bar. + + + + Primary action + + + + Secondary action + + + Secondary action 2 + + + + + + $250 + Given this month + + + $1,000 + Given this year + + + $1,300 + Given all time + + + + + diff --git a/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.ts b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.ts new file mode 100644 index 0000000000..81c59ccdf6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/action-bars/summary-action-bar/tab/example.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { SkySummaryActionBarModule } from '@skyux/action-bars'; +import { SkyKeyInfoModule } from '@skyux/indicators'; +import { SkyTabsModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-action-bars-summary-action-bar-tab-example', + templateUrl: './example.component.html', + imports: [SkyKeyInfoModule, SkySummaryActionBarModule, SkyTabsModule], +}) +export class ActionBarsSummaryActionBarTabExampleComponent { + protected onPrimaryActionClick(): void { + alert('Primary action button clicked.'); + } + + protected onSecondaryActionClick(): void { + alert('Secondary action button clicked.'); + } + + protected onSecondaryAction2Click(): void { + alert('Secondary action 2 button clicked.'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.ts new file mode 100644 index 0000000000..14ac840d30 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + protected contextMenuAriaLabel = ''; + protected deleteAriaLabel = ''; + protected markInactiveAriaLabel = ''; + protected moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + console.error(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/data.ts new file mode 100644 index 0000000000..c19a452159 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/data.ts @@ -0,0 +1,156 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal-context.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal-context.ts new file mode 100644 index 0000000000..227cc8ec5e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal-context.ts @@ -0,0 +1,5 @@ +import { AgGridDemoRow } from './data'; + +export class EditModalContext { + public gridData: AgGridDemoRow[] = []; +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.html new file mode 100644 index 0000000000..b0d8d47379 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.ts new file mode 100644 index 0000000000..5415f12e02 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/edit-modal.component.ts @@ -0,0 +1,236 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnInit, + TemplateRef, + ViewChild, + inject, +} from '@angular/core'; +import { + SkyAgGridAutocompleteProperties, + SkyAgGridDatepickerProperties, + SkyAgGridModule, + SkyAgGridService, + SkyCellType, +} from '@skyux/ag-grid'; +import { SkyAutocompleteSelectionChange } from '@skyux/lookup'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ICellEditorParams, + IRowNode, + NewValueParams, +} from 'ag-grid-community'; + +import { AgGridDemoRow, DEPARTMENTS, JOB_TITLES } from './data'; +import { EditModalContext } from './edit-modal-context'; +import { MarkInactiveComponent } from './mark-inactive.component'; + +@Component({ + selector: 'app-edit-modal', + templateUrl: './edit-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AgGridModule, + SkyAgGridModule, + SkyModalModule, + MarkInactiveComponent, + ], +}) +export class EditModalComponent implements OnInit { + @ViewChild('markInactiveAction', { static: true }) + protected markInactiveAction: TemplateRef | undefined; + + protected gridData: AgGridDemoRow[]; + protected gridOptions: GridOptions | undefined; + + #gridApi: GridApi | undefined; + + protected readonly instance = inject(SkyModalInstance); + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #context = inject(EditModalContext); + + constructor() { + this.gridData = this.#context.gridData; + } + + public ngOnInit(): void { + const columnDefs: ColDef[] = [ + { + colId: 'markInactiveAction', + headerName: 'Mark inactive', + type: SkyCellType.Template, + editable: true, + cellRendererParams: { + template: this.markInactiveAction, + }, + }, + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + cellRendererParams: { + skyComponentProperties: { + validator: (value: string): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + cellEditorParams: { + skyComponentProperties: { + maxlength: 10, + }, + }, + editable: true, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + cellRendererParams: { + skyComponentProperties: { + validator: (value: number): boolean => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + cellEditorParams: { + skyComponentProperties: { + min: 18, + }, + }, + editable: true, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { + return { skyComponentProperties: { minDate: params.data.startDate } }; + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + return { + skyComponentProperties: { + data: DEPARTMENTS, + selectionChange: ( + change: SkyAutocompleteSelectionChange, + ): void => { + this.#departmentSelectionChange(change, params.node); + }, + }, + }; + }, + onCellValueChanged: (event: NewValueParams): void => { + if (event.newValue !== event.oldValue) { + this.#clearJobTitle(event.node); + } + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + const selectedDepartment = params.data?.department?.name; + + const editParams: { + skyComponentProperties: SkyAgGridAutocompleteProperties; + } = { skyComponentProperties: { data: [] } }; + + if (selectedDepartment) { + editParams.skyComponentProperties.data = + JOB_TITLES[selectedDepartment]; + } + + return editParams; + }, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + editable: true, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Please enter a future date', + }, + }, + editable: true, + }, + ]; + + this.gridOptions = { + columnDefs: columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + }; + + this.gridOptions = this.#agGridSvc.getEditableGridOptions({ + gridOptions: this.gridOptions, + }); + + this.#changeDetectorRef.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + + this.#changeDetectorRef.markForCheck(); + } + + #departmentSelectionChange( + change: SkyAutocompleteSelectionChange, + node: IRowNode, + ): void { + if (change.selectedItem && change.selectedItem !== node.data?.department) { + this.#clearJobTitle(node); + } + } + + #clearJobTitle(node: IRowNode | null): void { + if (node?.data) { + node.data.jobTitle = undefined; + this.#changeDetectorRef.markForCheck(); + + if (this.#gridApi) { + this.#gridApi.refreshCells({ rowNodes: [node] }); + } + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.html new file mode 100644 index 0000000000..361bdebd88 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.html @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.ts new file mode 100644 index 0000000000..7e999e718f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/example.component.ts @@ -0,0 +1,209 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkySearchModule } from '@skyux/lookup'; +import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { of } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; +import { EditModalContext } from './edit-modal-context'; +import { EditModalComponent } from './edit-modal.component'; + +@Component({ + selector: 'app-ag-grid-data-entry-grid-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule, SkySearchModule, SkyToolbarModule], +}) +export class AgGridDataEntryGridBasicExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + protected noRowsTemplate = `
No results found.
`; + protected searchText = ''; + + #columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + headerName: 'Context menu', + headerComponentParams: { + headerHidden: true, + }, + }, + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + cellRendererParams: { + skyComponentProperties: { + validator: (value: string): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + cellRendererParams: { + skyComponentProperties: { + validator: (value: number): boolean => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Enter a future date', + }, + }, + }, + ]; + + #gridApi: GridApi | undefined; + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #modalSvc = inject(SkyModalService); + + constructor() { + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + }; + + this.gridOptions = this.#agGridSvc.getEditableGridOptions({ + gridOptions, + }); + + this.#changeDetectorRef.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#changeDetectorRef.markForCheck(); + } + + protected openModal(): void { + const context = new EditModalContext(); + + context.gridData = this.gridData; + + const options: SkyModalConfigurationInterface = { + ariaDescribedBy: 'docs-edit-grid-modal-content', + providers: [ + { + provide: EditModalContext, + useValue: context, + }, + ], + size: 'large', + }; + + const modalInstance = this.#modalSvc.open(EditModalComponent, options); + + modalInstance.closed.subscribe((result) => { + if (result.reason === 'cancel' || result.reason === 'close') { + alert('Edits canceled!'); + } else { + this.gridData = result.data as AgGridDemoRow[]; + + if (this.#gridApi) { + this.#gridApi.refreshCells(); + } + + alert('Saving data!'); + } + }); + } + + protected searchApplied(searchText: string | void): void { + this.searchText = searchText ?? ''; + + if (this.#gridApi) { + this.#gridApi.updateGridOptions({ quickFilterText: this.searchText }); + + const displayedRowCount = this.#gridApi.getDisplayedRowCount(); + + if (displayedRowCount > 0) { + this.#gridApi.hideOverlay(); + } else { + this.#gridApi.showNoRowsOverlay(); + } + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.html new file mode 100644 index 0000000000..078463722c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.html @@ -0,0 +1,8 @@ + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.ts new file mode 100644 index 0000000000..2278ef4273 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/basic/mark-inactive.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-mark-inactive', + templateUrl: './mark-inactive.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MarkInactiveComponent { + @Input() + public set name(value: string | undefined) { + this.#_name = value; + if (value) { + this.markInactiveAriaLabel = `Mark ${value} inactive`; + } + } + + public get name(): string | undefined { + return this.#_name; + } + + protected markInactiveAriaLabel = ''; + + #_name: string | undefined; + + protected markInactive(): void { + console.error(`Mark inactive action clicked for ${this.name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/data.ts new file mode 100644 index 0000000000..9fad992bc1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/data.ts @@ -0,0 +1,157 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + jobTitle: JOB_TITLES['Sales'][1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal-context.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal-context.ts new file mode 100644 index 0000000000..227cc8ec5e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal-context.ts @@ -0,0 +1,5 @@ +import { AgGridDemoRow } from './data'; + +export class EditModalContext { + public gridData: AgGridDemoRow[] = []; +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.html new file mode 100644 index 0000000000..b194f4eb80 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.html @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts new file mode 100644 index 0000000000..60587e8cb0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/edit-modal.component.ts @@ -0,0 +1,193 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { + SkyAgGridAutocompleteProperties, + SkyAgGridDatepickerProperties, + SkyAgGridModule, + SkyAgGridService, + SkyCellType, +} from '@skyux/ag-grid'; +import { SkyAutocompleteSelectionChange } from '@skyux/lookup'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ICellEditorParams, + IRowNode, +} from 'ag-grid-community'; + +import { AgGridDemoRow, DEPARTMENTS, JOB_TITLES } from './data'; +import { EditModalContext } from './edit-modal-context'; + +@Component({ + selector: 'app-edit-modal', + templateUrl: './edit-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule, SkyModalModule], +}) +export class EditModalComponent { + protected gridData: AgGridDemoRow[]; + protected gridOptions: GridOptions; + + #columnDefs: ColDef[]; + #gridApi: GridApi | undefined; + + protected readonly instance = inject(SkyModalInstance); + readonly #agGridService = inject(SkyAgGridService); + readonly #changeDetector = inject(ChangeDetectorRef); + readonly #context = inject(EditModalContext); + + constructor() { + this.#columnDefs = [ + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + editable: true, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { + return { + skyComponentProperties: { + minDate: params.data.startDate, + }, + }; + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + return { + skyComponentProperties: { + data: DEPARTMENTS, + selectionChange: (change): void => { + this.#departmentSelectionChange(change, params.node); + }, + }, + }; + }, + onCellValueChanged: (event): void => { + if (event.newValue !== event.oldValue) { + this.#clearJobTitle(event.node); + } + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + const selectedDepartment = params.data?.department?.name; + + const editParams: { + skyComponentProperties: SkyAgGridAutocompleteProperties; + } = { skyComponentProperties: { data: [] } }; + + if (selectedDepartment) { + editParams.skyComponentProperties.data = + JOB_TITLES[selectedDepartment]; + } + + return editParams; + }, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + editable: true, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Enter a future date.', + }, + }, + editable: true, + }, + ]; + + this.gridData = this.#context.gridData; + + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + }; + + this.gridOptions = this.#agGridService.getEditableGridOptions({ + gridOptions, + }); + + this.#changeDetector.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#changeDetector.markForCheck(); + } + + #departmentSelectionChange( + change: SkyAutocompleteSelectionChange, + node: IRowNode, + ): void { + if (change.selectedItem && change.selectedItem !== node.data?.department) { + this.#clearJobTitle(node); + } + } + + #clearJobTitle(node: IRowNode | null): void { + if (node?.data) { + node.data.jobTitle = undefined; + + if (this.#gridApi) { + this.#gridApi.refreshCells({ + rowNodes: [node], + }); + } + } + + this.#changeDetector.markForCheck(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.html new file mode 100644 index 0000000000..e9b22b4b2f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.html @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.ts new file mode 100644 index 0000000000..c450d8dbdd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component.ts @@ -0,0 +1,141 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; +import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; + +import { Subject, takeUntil } from 'rxjs'; + +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; +import { EditModalContext } from './edit-modal-context'; +import { EditModalComponent } from './edit-modal.component'; +import { FilterModalComponent } from './filter-modal.component'; +import { ViewGridComponent } from './view-grid.component'; + +@Component({ + selector: 'app-ag-grid-data-entry-grid-data-manager-added-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [ViewGridComponent, SkyDataManagerModule], +}) +export class AgGridDataEntryGridDataManagerAddedExampleComponent + implements OnInit, OnDestroy +{ + protected items = AG_GRID_DEMO_DATA; + + #activeViewId = 'dataEntryGridWithDataManagerView'; + + #defaultDataState = new SkyDataManagerState({ + filterData: { + filtersApplied: false, + filters: { + hideSales: false, + }, + }, + views: [ + { + viewId: 'dataEntryGridWithDataManagerView', + displayedColumnIds: [ + 'selected', + 'context', + 'name', + 'age', + 'startDate', + 'endDate', + 'department', + 'jobTitle', + 'validationCurrency', + 'validationDate', + ], + }, + ], + }); + + #dataManagerConfig = { + filterModalComponent: FilterModalComponent, + sortOptions: [ + { + id: 'az', + label: 'Name (A - Z)', + descending: false, + propertyName: 'name', + }, + { + id: 'za', + label: 'Name (Z - A)', + descending: true, + propertyName: 'name', + }, + ], + }; + + #ngUnsubscribe = new Subject(); + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + readonly #modalSvc = inject(SkyModalService); + + constructor() { + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((activeViewId) => { + this.#activeViewId = activeViewId; + this.#changeDetectorRef.detectChanges(); + }); + } + + public ngOnInit(): void { + this.#dataManagerSvc.initDataManager({ + activeViewId: this.#activeViewId, + dataManagerConfig: this.#dataManagerConfig, + defaultDataState: this.#defaultDataState, + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + protected openModal(): void { + const context = new EditModalContext(); + context.gridData = this.items.slice(); + + this.#changeDetectorRef.markForCheck(); + + const options: SkyModalConfigurationInterface = { + ariaDescribedBy: 'docs-edit-grid-modal-content', + providers: [ + { + provide: EditModalContext, + useValue: context, + }, + ], + size: 'large', + }; + + const modalInstance = this.#modalSvc.open(EditModalComponent, options); + + modalInstance.closed.subscribe((result) => { + if (result.reason === 'cancel' || result.reason === 'close') { + alert('Edits canceled!'); + } else { + this.items = result.data as AgGridDemoRow[]; + alert('Saving data!'); + } + + this.#changeDetectorRef.markForCheck(); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.html new file mode 100644 index 0000000000..d8377f355b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.html @@ -0,0 +1,37 @@ + + + + +
+ +
+
+ + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts new file mode 100644 index 0000000000..0fa00f07f6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filter-modal.component.ts @@ -0,0 +1,65 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { + SkyDataManagerFilterData, + SkyDataManagerFilterModalContext, +} from '@skyux/data-manager'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { Filters } from './filters'; + +@Component({ + selector: 'app-filter-modal', + templateUrl: './filter-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FormsModule, SkyCheckboxModule, SkyIdModule, SkyModalModule], +}) +export class FilterModalComponent { + protected hideSales = false; + protected jobTitle = ''; + + readonly #changeDetector = inject(ChangeDetectorRef); + readonly #context = inject(SkyDataManagerFilterModalContext); + readonly #instance = inject(SkyModalInstance); + + constructor() { + if (this.#context.filterData?.filters) { + const filters = this.#context.filterData.filters as Filters; + + this.jobTitle = filters.jobTitle ?? 'any'; + this.hideSales = filters.hideSales ?? false; + } + + this.#changeDetector.markForCheck(); + } + + protected applyFilters(): void { + const result: SkyDataManagerFilterData = {}; + + result.filtersApplied = this.jobTitle !== 'any' || this.hideSales; + result.filters = { + jobTitle: this.jobTitle, + hideSales: this.hideSales, + } satisfies Filters; + + this.#changeDetector.markForCheck(); + this.#instance.save(result); + } + + protected clearAllFilters(): void { + this.hideSales = false; + this.jobTitle = 'any'; + this.#changeDetector.markForCheck(); + } + + protected cancel(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filters.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filters.ts new file mode 100644 index 0000000000..bbf4a0fe8d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + jobTitle?: string; + hideSales?: boolean; +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.html new file mode 100644 index 0000000000..27aafd3719 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.html @@ -0,0 +1,12 @@ + + @if (isActive && isGridInitialized) { + + + + } + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts new file mode 100644 index 0000000000..0a9c774a5a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/data-manager-added/view-grid.component.ts @@ -0,0 +1,352 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, +} from '@skyux/data-manager'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + RowSelectedEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AgGridDemoRow } from './data'; +import { Filters } from './filters'; + +@Component({ + selector: 'app-view-grid', + templateUrl: './view-grid.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule], +}) +export class ViewGridComponent implements OnInit, OnDestroy { + @Input() + public set items(value: AgGridDemoRow[]) { + this.#_items = value; + this.#changeDetectorRef.markForCheck(); + this.#gridApi?.refreshCells(); + } + + public get items(): AgGridDemoRow[] { + return this.#_items; + } + + #viewId = 'dataEntryGridWithDataManagerView'; + + #columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + headerName: 'Context menu', + headerComponentParams: { + headerHidden: true, + }, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + { + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + }, + { + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Please enter a future date', + }, + }, + }, + ]; + + protected displayedItems: AgGridDemoRow[] = []; + protected gridOptions: GridOptions; + protected isActive = false; + protected isGridInitialized = false; + protected noRowsTemplate = `
No results found.
`; + protected viewConfig: SkyDataViewConfig = { + id: this.#viewId, + name: 'Data Grid View', + icon: 'table', + searchEnabled: true, + multiselectToolbarEnabled: true, + columnPickerEnabled: true, + filterButtonEnabled: true, + columnOptions: [ + { + id: 'selected', + label: '', + alwaysDisplayed: true, + }, + { + id: 'context', + label: '', + alwaysDisplayed: true, + }, + { + id: 'name', + label: 'Name', + description: 'The name of the employee.', + }, + { + id: 'age', + label: 'Age', + description: 'The age of the employee.', + }, + { + id: 'startDate', + label: 'Start date', + description: 'The start date of the employee.', + }, + { + id: 'endDate', + label: 'End date', + description: 'The end date of the employee.', + }, + { + id: 'department', + label: 'Department', + description: 'The department of the employee', + }, + { + id: 'jobTitle', + label: 'Title', + description: 'The job title of the employee.', + }, + { + id: 'validationCurrency', + label: 'Validation currency', + description: 'An example column for currency validation.', + }, + { + id: 'validationDate', + label: 'Validation date', + description: 'An example column for date validation.', + }, + ], + }; + + #_items: AgGridDemoRow[] = []; + + #dataState = new SkyDataManagerState({}); + #gridApi: GridApi | undefined; + #ngUnsubscribe = new Subject(); + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + constructor() { + this.gridOptions = this.#agGridSvc.getEditableGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + onGridReady: this.onGridReady.bind(this), + }, + }); + } + + public ngOnInit(): void { + this.displayedItems = this.items; + + this.#dataManagerSvc.initDataView(this.viewConfig); + + this.#dataManagerSvc + .getDataStateUpdates(this.#viewId) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((state) => { + this.#dataState = state; + this.#setInitialColumnOrder(); + this.#updateData(); + this.#changeDetectorRef.detectChanges(); + }); + + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((id) => { + this.isActive = id === this.#viewId; + this.#changeDetectorRef.detectChanges(); + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + protected onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#updateData(); + this.#changeDetectorRef.markForCheck(); + } + + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { + this.#updateData(); + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } + + #filterItems(items: AgGridDemoRow[]): AgGridDemoRow[] { + let filteredItems = items; + const filterData = this.#dataState.filterData; + + if (filterData?.filters) { + const filters = filterData.filters as Filters; + + filteredItems = items.filter((item) => { + return ( + (!!(filters.hideSales && item.department.name !== 'Sales') || + !filters.hideSales) && + ((filters.jobTitle !== 'any' && + item.jobTitle?.name === filters.jobTitle) || + !filters.jobTitle || + filters.jobTitle === 'any') + ); + }); + } + + return filteredItems; + } + + #searchItems(items: AgGridDemoRow[]): AgGridDemoRow[] { + let searchedItems = items; + const searchText = this.#dataState.searchText; + + if (searchText) { + searchedItems = items.filter((item) => { + let property: keyof typeof item; + for (property in item) { + if ( + Object.prototype.hasOwnProperty.call(item, property) && + property === 'name' + ) { + const propertyText = item[property]?.toLowerCase(); + if (propertyText.includes(searchText)) { + return true; + } + } + } + + return false; + }); + } + + return searchedItems; + } + + #setInitialColumnOrder(): void { + const viewState = this.#dataState.getViewStateById(this.#viewId); + const visibleColumns = viewState?.displayedColumnIds ?? []; + + this.#columnDefs.sort((col1, col2) => { + const col1Index = visibleColumns.findIndex( + (colId: string) => colId === col1.colId, + ); + const col2Index = visibleColumns.findIndex( + (colId: string) => colId === col2.colId, + ); + + if (col1Index === -1) { + col1.hide = true; + return 0; + } else if (col2Index === -1) { + col2.hide = true; + return 0; + } else { + return col1Index - col2Index; + } + }); + + this.isGridInitialized = true; + this.#changeDetectorRef.markForCheck(); + } + + #updateData(): void { + this.displayedItems = this.#filterItems(this.#searchItems(this.items)); + + if (this.#dataState.onlyShowSelected) { + this.displayedItems = this.displayedItems.filter((item) => item.selected); + } + + if (this.displayedItems.length > 0) { + this.#gridApi?.hideOverlay(); + } else { + this.#gridApi?.showNoRowsOverlay(); + } + + this.#changeDetectorRef.markForCheck(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/data.ts new file mode 100644 index 0000000000..c19a452159 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/data.ts @@ -0,0 +1,156 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.html new file mode 100644 index 0000000000..85437ba939 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.html @@ -0,0 +1,23 @@ + + + +
+ + + +
+ + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.ts new file mode 100644 index 0000000000..ea8b77bf22 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/focus/example.component.ts @@ -0,0 +1,114 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyInputBoxModule } from '@skyux/forms'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ValueFormatterParams } from 'ag-grid-community'; + +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-ag-grid-data-entry-grid-focus-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule, SkyInputBoxModule], +}) +export class AgGridDataEntryGridFocusExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions = inject(SkyAgGridService).getEditableGridOptions({ + gridOptions: { + columnDefs: [ + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + editable: true, + cellRendererParams: { + skyComponentProperties: { + validator: (value: string): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + editable: true, + cellRendererParams: { + skyComponentProperties: { + validator: (value: number): boolean => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + editable: true, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + editable: true, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + editable: true, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + editable: true, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + editable: true, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + editable: true, + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Enter a future date', + }, + }, + }, + ], + focusGridInnerElement: (params) => { + params.api.startEditingCell({ + rowIndex: 0, + colKey: 'name', + }); + return true; + }, + }, + }); + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/data.ts new file mode 100644 index 0000000000..c19a452159 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/data.ts @@ -0,0 +1,156 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal-context.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal-context.ts new file mode 100644 index 0000000000..227cc8ec5e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal-context.ts @@ -0,0 +1,5 @@ +import { AgGridDemoRow } from './data'; + +export class EditModalContext { + public gridData: AgGridDemoRow[] = []; +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.html new file mode 100644 index 0000000000..b194f4eb80 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.html @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts new file mode 100644 index 0000000000..18f1d218e0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/edit-modal.component.ts @@ -0,0 +1,207 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { + SkyAgGridAutocompleteProperties, + SkyAgGridDatepickerProperties, + SkyAgGridModule, + SkyAgGridService, + SkyCellType, +} from '@skyux/ag-grid'; +import { SkyAutocompleteSelectionChange } from '@skyux/lookup'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ICellEditorParams, + IRowNode, +} from 'ag-grid-community'; + +import { AgGridDemoRow, DEPARTMENTS, JOB_TITLES } from './data'; +import { EditModalContext } from './edit-modal-context'; + +@Component({ + selector: 'app-edit-modal', + templateUrl: './edit-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule, SkyModalModule], +}) +export class EditModalComponent { + protected gridData: AgGridDemoRow[]; + protected gridOptions: GridOptions; + + #columnDefs: ColDef[]; + #gridApi: GridApi | undefined; + + protected readonly instance = inject(SkyModalInstance); + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #context = inject(EditModalContext); + + constructor() { + this.#columnDefs = [ + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + cellRendererParams: { + skyComponentProperties: { + validator: (value: string): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + cellEditorParams: { + skyComponentProperties: { + maxlength: 10, + }, + }, + editable: true, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + cellRendererParams: { + skyComponentProperties: { + validator: (value: number): boolean => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + cellEditorParams: { + skyComponentProperties: { + min: 18, + }, + }, + editable: true, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridDatepickerProperties } => { + return { skyComponentProperties: { minDate: params.data.startDate } }; + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + return { + skyComponentProperties: { + data: DEPARTMENTS, + selectionChange: (change): void => { + this.#departmentSelectionChange(change, params.node); + }, + }, + }; + }, + onCellValueChanged: (event): void => { + if (event.newValue !== event.oldValue) { + this.#clearJobTitle(event.node); + } + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + editable: true, + cellEditorParams: ( + params: ICellEditorParams, + ): { skyComponentProperties: SkyAgGridAutocompleteProperties } => { + const selectedDepartment = params.data?.department?.name; + const editParams: { + skyComponentProperties: SkyAgGridAutocompleteProperties; + } = { skyComponentProperties: { data: [] } }; + + if (selectedDepartment) { + editParams.skyComponentProperties.data = + JOB_TITLES[selectedDepartment]; + } + + return editParams; + }, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + editable: true, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Please enter a future date', + }, + }, + editable: true, + }, + ]; + + this.gridData = this.#context.gridData; + + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + }; + + this.gridOptions = this.#agGridSvc.getEditableGridOptions({ + gridOptions, + }); + + this.#changeDetectorRef.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#changeDetectorRef.markForCheck(); + } + + #departmentSelectionChange( + change: SkyAutocompleteSelectionChange, + node: IRowNode, + ): void { + if (change.selectedItem && change.selectedItem !== node.data?.department) { + this.#clearJobTitle(node); + } + } + + #clearJobTitle(node: IRowNode | null): void { + if (node?.data) { + node.data.jobTitle = undefined; + + this.#changeDetectorRef.markForCheck(); + this.#gridApi?.refreshCells({ rowNodes: [node] }); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.html new file mode 100644 index 0000000000..355398e13e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.ts new file mode 100644 index 0000000000..8677977b2f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/example.component.ts @@ -0,0 +1,232 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkySearchModule } from '@skyux/lookup'; +import { + SkyModalCloseArgs, + SkyModalConfigurationInterface, + SkyModalService, +} from '@skyux/modals'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { of } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; +import { EditModalContext } from './edit-modal-context'; +import { EditModalComponent } from './edit-modal.component'; +import { InlineHelpComponent } from './inline-help.component'; + +@Component({ + selector: 'app-ag-grid-data-entry-grid-inline-help-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule, SkySearchModule, SkyToolbarModule], +}) +export class AgGridDataEntryGridInlineHelpExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + protected noRowsTemplate = `
No results found.
`; + protected searchText = ''; + + #columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + headerName: 'Context menu', + headerComponentParams: { + headerHidden: true, + }, + }, + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + cellRendererParams: { + skyComponentProperties: { + validator: (value: string): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + cellRendererParams: { + skyComponentProperties: { + validator: (value: number): boolean => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Enter a future date', + }, + }, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + ]; + #gridApi: GridApi | undefined; + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #modalSvc = inject(SkyModalService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + + constructor() { + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + }; + + this.gridOptions = this.#agGridSvc.getEditableGridOptions({ + gridOptions, + }); + + this.#changeDetectorRef.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#changeDetectorRef.markForCheck(); + } + + protected openModal(): void { + const context = new EditModalContext(); + context.gridData = this.gridData; + + const options: SkyModalConfigurationInterface = { + providers: [ + { + provide: EditModalContext, + useValue: context, + }, + ], + ariaDescribedBy: 'docs-edit-grid-modal-content', + size: 'large', + }; + + const modalInstance = this.#modalSvc.open(EditModalComponent, options); + + modalInstance.closed.subscribe((result: SkyModalCloseArgs) => { + if (result.reason === 'cancel' || result.reason === 'close') { + alert('Edits canceled!'); + } else { + this.gridData = result.data as AgGridDemoRow[]; + this.#gridApi?.refreshCells(); + + alert('Saving data!'); + } + }); + } + + protected searchApplied(searchText: string | void): void { + this.searchText = searchText ?? ''; + + if (this.#gridApi) { + this.#gridApi.updateGridOptions({ quickFilterText: this.searchText }); + const displayedRowCount = this.#gridApi.getDisplayedRowCount(); + + if (displayedRowCount > 0) { + this.#gridApi.hideOverlay(); + } else { + this.#gridApi.showNoRowsOverlay(); + } + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.html new file mode 100644 index 0000000000..2286356e86 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.html @@ -0,0 +1,5 @@ + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..55ed9a7b8d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-entry-grid/inline-help/inline-help.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyHelpInlineModule], +}) +export class InlineHelpComponent { + protected displayName: string | undefined; + + readonly #headerInfo = inject(SkyAgGridHeaderInfo); + + constructor() { + this.displayName = this.#headerInfo.displayName; + } + + protected onHelpClick(): void { + alert(`Help was clicked for ${this.displayName}.`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/data.ts new file mode 100644 index 0000000000..bacae1b9e0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/data.ts @@ -0,0 +1,156 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + selected: false, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.html new file mode 100644 index 0000000000..d942b0d49f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.html @@ -0,0 +1,3 @@ + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.ts new file mode 100644 index 0000000000..64b0c8bb70 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic-multiselect/example.component.ts @@ -0,0 +1,94 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyDataManagerService } from '@skyux/data-manager'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ColDef, GridOptions, ValueFormatterParams } from 'ag-grid-community'; +import { of } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-ag-grid-data-grid-basic-multiselect-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [AgGridModule, SkyAgGridModule], +}) +export class AgGridDataGridBasicMultiselectExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + + #columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + ]; + + readonly #agGridSvc = inject(SkyAgGridService); + + constructor() { + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + }; + + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions, + }); + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/data.ts new file mode 100644 index 0000000000..6652bfbb50 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/data.ts @@ -0,0 +1,149 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.html new file mode 100644 index 0000000000..d942b0d49f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.html @@ -0,0 +1,3 @@ + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.ts new file mode 100644 index 0000000000..b650e921da --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/basic/example.component.ts @@ -0,0 +1,86 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyDataManagerService } from '@skyux/data-manager'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ColDef, GridOptions, ValueFormatterParams } from 'ag-grid-community'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-ag-grid-data-grid-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [AgGridModule, SkyAgGridModule], +}) +export class AgGridDataGridBasicExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + + #columnDefs: ColDef[] = [ + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + ]; + + readonly #agGridSvc = inject(SkyAgGridService); + + constructor() { + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + rowSelection: { mode: 'singleRow' }, + }; + + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions, + }); + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/data.ts new file mode 100644 index 0000000000..6dd8a948dd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/data.ts @@ -0,0 +1,157 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + selected: false, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + jobTitle: JOB_TITLES['Sales'][1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.html new file mode 100644 index 0000000000..d55b157e72 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.ts new file mode 100644 index 0000000000..99faea7f1b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/example.component.ts @@ -0,0 +1,106 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { + SkyDataManagerConfig, + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; + +import { Subject, takeUntil } from 'rxjs'; + +import { AG_GRID_DEMO_DATA } from './data'; +import { FilterModalComponent } from './filter-modal.component'; +import { Filters } from './filters'; +import { ViewGridComponent } from './view-grid.component'; + +@Component({ + selector: 'app-ag-grid-data-grid-data-manager-multiselect-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [SkyDataManagerModule, ViewGridComponent], +}) +export class AgGridDataGridDataManagerMultiselectExampleComponent + implements OnInit, OnDestroy +{ + protected items = AG_GRID_DEMO_DATA; + + #dataManagerConfig: SkyDataManagerConfig = { + filterModalComponent: FilterModalComponent, + sortOptions: [ + { + id: 'az', + label: 'Name (A - Z)', + descending: false, + propertyName: 'name', + }, + { + id: 'za', + label: 'Name (Z - A)', + descending: true, + propertyName: 'name', + }, + ], + }; + + #defaultDataState = new SkyDataManagerState({ + filterData: { + filtersApplied: false, + filters: { + hideSales: false, + jobTitle: 'any', + } satisfies Filters, + }, + views: [ + { + viewId: 'dataGridMultiselectWithDataManagerView', + displayedColumnIds: [ + 'selected', + 'context', + 'name', + 'age', + 'startDate', + 'endDate', + 'department', + 'jobTitle', + ], + }, + ], + }); + + #activeViewId = 'dataGridMultiselectWithDataManagerView'; + #ngUnsubscribe = new Subject(); + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + constructor() { + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((activeViewId) => { + this.#activeViewId = activeViewId; + this.#changeDetectorRef.detectChanges(); + }); + } + + public ngOnInit(): void { + this.#dataManagerSvc.initDataManager({ + activeViewId: this.#activeViewId, + dataManagerConfig: this.#dataManagerConfig, + defaultDataState: this.#defaultDataState, + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.html new file mode 100644 index 0000000000..d8377f355b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.html @@ -0,0 +1,37 @@ + + + + +
+ +
+
+ + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts new file mode 100644 index 0000000000..4f181d2880 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filter-modal.component.ts @@ -0,0 +1,65 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { + SkyDataManagerFilterData, + SkyDataManagerFilterModalContext, +} from '@skyux/data-manager'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { Filters } from './filters'; + +@Component({ + selector: 'app-filter-modal', + templateUrl: './filter-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FormsModule, SkyCheckboxModule, SkyIdModule, SkyModalModule], +}) +export class FilterModalComponent { + protected hideSales = false; + protected jobTitle = ''; + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #context = inject(SkyDataManagerFilterModalContext); + readonly #instance = inject(SkyModalInstance); + + constructor() { + if (this.#context.filterData && this.#context.filterData.filters) { + const filters = this.#context.filterData.filters as Filters; + + this.jobTitle = filters.jobTitle ?? 'any'; + this.hideSales = filters.hideSales ?? false; + } + + this.#changeDetectorRef.markForCheck(); + } + + protected applyFilters(): void { + const result: SkyDataManagerFilterData = {}; + + result.filtersApplied = this.jobTitle !== 'any' || this.hideSales; + result.filters = { + jobTitle: this.jobTitle, + hideSales: this.hideSales, + } satisfies Filters; + + this.#changeDetectorRef.markForCheck(); + this.#instance.save(result); + } + + protected clearAllFilters(): void { + this.hideSales = false; + this.jobTitle = 'any'; + this.#changeDetectorRef.markForCheck(); + } + + protected cancel(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filters.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filters.ts new file mode 100644 index 0000000000..bbf4a0fe8d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + jobTitle?: string; + hideSales?: boolean; +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.html new file mode 100644 index 0000000000..d1c8155c07 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.html @@ -0,0 +1,12 @@ + + @if (isViewActive && isGridReadyForInitialization) { + + + + } + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts new file mode 100644 index 0000000000..981b7c5cb9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager-multiselect/view-grid.component.ts @@ -0,0 +1,340 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { + SkyDataManagerColumnPickerOption, + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, + SkyDataViewState, +} from '@skyux/data-manager'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + RowSelectedEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AgGridDemoRow } from './data'; +import { Filters } from './filters'; + +@Component({ + selector: 'app-view-grid', + templateUrl: './view-grid.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule], +}) +export class ViewGridComponent implements OnInit, OnDestroy { + @Input() + public items: AgGridDemoRow[] = []; + + protected displayedItems: AgGridDemoRow[] = []; + protected gridOptions!: GridOptions; + protected isGridReadyForInitialization = false; + protected isViewActive = false; + protected noRowsTemplate = `
No results found.
`; + protected viewConfig: SkyDataViewConfig; + + #columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + ]; + + #columnPickerOptions: SkyDataManagerColumnPickerOption[] = [ + { + id: 'selected', + label: '', + alwaysDisplayed: true, + }, + { + id: 'context', + label: '', + alwaysDisplayed: true, + }, + { + id: 'name', + label: 'Name', + description: 'The name of the employee.', + }, + { + id: 'age', + label: 'Age', + description: 'The age of the employee.', + }, + { + id: 'startDate', + label: 'Start date', + description: 'The start date of the employee.', + }, + { + id: 'endDate', + label: 'End date', + description: 'The end date of the employee.', + }, + { + id: 'department', + label: 'Department', + description: 'The department of the employee', + }, + { + id: 'jobTitle', + label: 'Title', + description: 'The job title of the employee.', + }, + ]; + + #dataState = new SkyDataManagerState({}); + #gridApi?: GridApi; + #ngUnsubscribe = new Subject(); + #viewId = 'dataGridMultiselectWithDataManagerView'; + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + constructor() { + this.viewConfig = { + id: this.#viewId, + name: 'Data Grid View', + icon: 'table', + searchEnabled: true, + multiselectToolbarEnabled: true, + columnPickerEnabled: true, + filterButtonEnabled: true, + columnOptions: this.#columnPickerOptions, + }; + } + + public ngOnInit(): void { + this.displayedItems = this.items; + + this.#dataManagerSvc.initDataView(this.viewConfig); + + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + onGridReady: this.onGridReady.bind(this), + }, + }); + + this.#dataManagerSvc + .getDataStateUpdates(this.#viewId) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((state) => { + this.#dataState = state; + this.#updateColumnOrder(); + this.isGridReadyForInitialization = true; + this.#updateDisplayedItems(); + this.#changeDetectorRef.detectChanges(); + }); + + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((id) => { + this.isViewActive = id === this.#viewId; + this.#changeDetectorRef.detectChanges(); + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#updateDisplayedItems(); + } + + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { + this.#updateDisplayedItems(); + } + } + + #searchItems(items: AgGridDemoRow[]): AgGridDemoRow[] { + let searchedItems = items; + const searchText = this.#dataState && this.#dataState.searchText; + + if (searchText) { + searchedItems = items.filter((item) => { + let property: keyof typeof item; + + for (property in item) { + if ( + Object.prototype.hasOwnProperty.call(item, property) && + property === 'name' + ) { + const propertyText = item[property].toLowerCase(); + if (propertyText.includes(searchText)) { + return true; + } + } + } + + return false; + }); + } + + return searchedItems; + } + + #filterItems(items: AgGridDemoRow[]): AgGridDemoRow[] { + let filteredItems = items; + const filterData = this.#dataState && this.#dataState.filterData; + + if (filterData?.filters) { + const filters = filterData.filters as Filters; + + filteredItems = items.filter((item) => { + return ( + (!!(filters.hideSales && item.department.name !== 'Sales') || + !filters.hideSales) && + ((filters.jobTitle !== 'any' && + item.jobTitle?.name === filters.jobTitle) || + !filters.jobTitle || + filters.jobTitle === 'any') + ); + }); + } + + return filteredItems; + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } + + #updateColumnOrder(): void { + const viewState: SkyDataViewState | undefined = + this.#dataState.getViewStateById(this.#viewId); + + if (viewState) { + this.#columnDefs.sort((columnDefinition1, columnDefinition2) => { + const displayedColumnIdIndex1: number = + viewState.displayedColumnIds.findIndex( + (aDisplayedColumnId: string) => + aDisplayedColumnId === columnDefinition1.field, + ); + + const displayedColumnIdIndex2: number = + viewState.displayedColumnIds.findIndex( + (aDisplayedColumnId: string) => + aDisplayedColumnId === columnDefinition2.field, + ); + + if (displayedColumnIdIndex1 === -1) { + return 0; + } else if (displayedColumnIdIndex2 === -1) { + return 0; + } else { + return displayedColumnIdIndex1 - displayedColumnIdIndex2; + } + }); + + this.#changeDetectorRef.markForCheck(); + } + } + + #updateDisplayedItems(): void { + const sortOption = this.#dataState.activeSortOption; + + if (this.#gridApi && sortOption) { + this.#gridApi.applyColumnState({ + state: [ + { + colId: sortOption.propertyName, + sort: sortOption.descending ? 'desc' : 'asc', + }, + ], + }); + } + + this.displayedItems = this.#filterItems(this.#searchItems(this.items)); + + if (this.#dataState.onlyShowSelected) { + this.displayedItems = this.displayedItems.filter((item) => item.selected); + } + + if (this.displayedItems.length > 0) { + this.#gridApi?.hideOverlay(); + } else { + this.#gridApi?.showNoRowsOverlay(); + } + + this.#dataManagerSvc.updateDataSummary( + { + totalItems: this.items.length, + itemsMatching: this.displayedItems.length, + }, + this.#viewId, + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/data.ts new file mode 100644 index 0000000000..5d709550a5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/data.ts @@ -0,0 +1,150 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + jobTitle: JOB_TITLES['Sales'][1], + }, + { + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.html new file mode 100644 index 0000000000..d55b157e72 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.ts new file mode 100644 index 0000000000..199903360f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/example.component.ts @@ -0,0 +1,105 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; + +import { Subject, takeUntil } from 'rxjs'; + +import { AG_GRID_DEMO_DATA } from './data'; +import { FilterModalComponent } from './filter-modal.component'; +import { Filters } from './filters'; +import { ViewGridComponent } from './view-grid.component'; + +@Component({ + selector: 'app-ag-grid-data-grid-data-manager-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [SkyDataManagerModule, ViewGridComponent], +}) +export class AgGridDataGridDataManagerExampleComponent + implements OnInit, OnDestroy +{ + protected items = AG_GRID_DEMO_DATA; + + #activeViewId = 'dataGridWithDataManagerView'; + + #dataManagerConfig = { + filterModalComponent: FilterModalComponent, + sortOptions: [ + { + id: 'az', + label: 'Name (A - Z)', + descending: false, + propertyName: 'name', + }, + { + id: 'za', + label: 'Name (Z - A)', + descending: true, + propertyName: 'name', + }, + ], + }; + + #defaultDataState = new SkyDataManagerState({ + filterData: { + filtersApplied: false, + filters: { + hideSales: false, + jobTitle: 'any', + } satisfies Filters, + }, + views: [ + { + viewId: 'dataGridWithDataManagerView', + displayedColumnIds: [ + 'context', + 'name', + 'age', + 'startDate', + 'endDate', + 'department', + 'jobTitle', + ], + }, + ], + }); + + #ngUnsubscribe = new Subject(); + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + constructor() { + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((activeViewId) => { + this.#activeViewId = activeViewId; + this.#changeDetectorRef.detectChanges(); + }); + } + + public ngOnInit(): void { + this.#dataManagerSvc.initDataManager({ + activeViewId: this.#activeViewId, + dataManagerConfig: this.#dataManagerConfig, + defaultDataState: this.#defaultDataState, + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.html new file mode 100644 index 0000000000..d8377f355b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.html @@ -0,0 +1,37 @@ + + + + +
+ +
+
+ + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.ts new file mode 100644 index 0000000000..4f181d2880 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filter-modal.component.ts @@ -0,0 +1,65 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { + SkyDataManagerFilterData, + SkyDataManagerFilterModalContext, +} from '@skyux/data-manager'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { Filters } from './filters'; + +@Component({ + selector: 'app-filter-modal', + templateUrl: './filter-modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [FormsModule, SkyCheckboxModule, SkyIdModule, SkyModalModule], +}) +export class FilterModalComponent { + protected hideSales = false; + protected jobTitle = ''; + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #context = inject(SkyDataManagerFilterModalContext); + readonly #instance = inject(SkyModalInstance); + + constructor() { + if (this.#context.filterData && this.#context.filterData.filters) { + const filters = this.#context.filterData.filters as Filters; + + this.jobTitle = filters.jobTitle ?? 'any'; + this.hideSales = filters.hideSales ?? false; + } + + this.#changeDetectorRef.markForCheck(); + } + + protected applyFilters(): void { + const result: SkyDataManagerFilterData = {}; + + result.filtersApplied = this.jobTitle !== 'any' || this.hideSales; + result.filters = { + jobTitle: this.jobTitle, + hideSales: this.hideSales, + } satisfies Filters; + + this.#changeDetectorRef.markForCheck(); + this.#instance.save(result); + } + + protected clearAllFilters(): void { + this.hideSales = false; + this.jobTitle = 'any'; + this.#changeDetectorRef.markForCheck(); + } + + protected cancel(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filters.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filters.ts new file mode 100644 index 0000000000..bbf4a0fe8d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + jobTitle?: string; + hideSales?: boolean; +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.html new file mode 100644 index 0000000000..d1c8155c07 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.html @@ -0,0 +1,12 @@ + + @if (isViewActive && isGridReadyForInitialization) { + + + + } + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.ts new file mode 100644 index 0000000000..4707f8847c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/data-manager/view-grid.component.ts @@ -0,0 +1,315 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { + SkyDataManagerColumnPickerOption, + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, + SkyDataViewState, +} from '@skyux/data-manager'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + RowSelectedEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { Subject, takeUntil } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AgGridDemoRow } from './data'; +import { Filters } from './filters'; + +@Component({ + selector: 'app-view-grid', + templateUrl: './view-grid.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule], +}) +export class ViewGridComponent implements OnInit, OnDestroy { + @Input() + public items: AgGridDemoRow[] = []; + + protected displayedItems: AgGridDemoRow[] = []; + protected gridOptions!: GridOptions; + protected isGridReadyForInitialization = false; + protected isViewActive = false; + protected noRowsTemplate = `
No results found.
`; + protected viewConfig: SkyDataViewConfig; + + #columnDefs: ColDef[] = [ + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + ]; + + #columnPickerOptions: SkyDataManagerColumnPickerOption[] = [ + { + id: 'context', + label: '', + alwaysDisplayed: true, + }, + { + id: 'name', + label: 'Name', + description: 'The name of the employee.', + }, + { + id: 'age', + label: 'Age', + description: 'The age of the employee.', + }, + { + id: 'startDate', + label: 'Start date', + description: 'The start date of the employee.', + }, + { + id: 'endDate', + label: 'End date', + description: 'The end date of the employee.', + }, + { + id: 'department', + label: 'Department', + description: 'The department of the employee', + }, + { + id: 'jobTitle', + label: 'Title', + description: 'The job title of the employee.', + }, + ]; + + #dataState = new SkyDataManagerState({}); + #gridApi: GridApi | undefined; + #ngUnsubscribe = new Subject(); + #viewId = 'dataGridWithDataManagerView'; + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + constructor() { + this.viewConfig = { + id: this.#viewId, + name: 'Data Grid View', + icon: 'table', + searchEnabled: true, + columnPickerEnabled: true, + filterButtonEnabled: true, + columnOptions: this.#columnPickerOptions, + }; + } + + public ngOnInit(): void { + this.displayedItems = this.items; + + this.#dataManagerSvc.initDataView(this.viewConfig); + + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + onGridReady: this.onGridReady.bind(this), + rowSelection: { mode: 'singleRow' }, + }, + }); + + this.#dataManagerSvc + .getDataStateUpdates(this.#viewId) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((state) => { + this.#dataState = state; + this.#updateColumnOrder(); + this.isGridReadyForInitialization = true; + this.#updateDisplayedItems(); + this.#changeDetectorRef.detectChanges(); + }); + + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((id) => { + this.isViewActive = id === this.#viewId; + this.#changeDetectorRef.detectChanges(); + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#updateDisplayedItems(); + } + + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { + this.#updateDisplayedItems(); + } + } + + #searchItems(items: AgGridDemoRow[]): AgGridDemoRow[] { + let searchedItems = items; + const searchText = this.#dataState && this.#dataState.searchText; + + if (searchText) { + searchedItems = items.filter((item: AgGridDemoRow) => { + let property: keyof typeof item; + + for (property in item) { + if ( + Object.prototype.hasOwnProperty.call(item, property) && + property === 'name' + ) { + const propertyText = item[property].toLowerCase(); + + if (propertyText.includes(searchText)) { + return true; + } + } + } + + return false; + }); + } + + return searchedItems; + } + + #filterItems(items: AgGridDemoRow[]): AgGridDemoRow[] { + let filteredItems = items; + const filterData = this.#dataState && this.#dataState.filterData; + + if (filterData?.filters) { + const filters = filterData.filters as Filters; + + filteredItems = items.filter((item: AgGridDemoRow) => { + return ( + (!!(filters.hideSales && item.department.name !== 'Sales') || + !filters.hideSales) && + ((filters.jobTitle !== 'any' && + item.jobTitle?.name === filters.jobTitle) || + !filters.jobTitle || + filters.jobTitle === 'any') + ); + }); + } + + return filteredItems; + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } + + #updateColumnOrder(): void { + const viewState: SkyDataViewState | undefined = + this.#dataState.getViewStateById(this.#viewId); + + if (viewState) { + this.#columnDefs.sort((columnDefinition1, columnDefinition2) => { + const displayedColumnIdIndex1: number = + viewState.displayedColumnIds.findIndex( + (aDisplayedColumnId: string) => + aDisplayedColumnId === columnDefinition1.field, + ); + + const displayedColumnIdIndex2: number = + viewState.displayedColumnIds.findIndex( + (aDisplayedColumnId: string) => + aDisplayedColumnId === columnDefinition2.field, + ); + + if (displayedColumnIdIndex1 === -1) { + return 0; + } else if (displayedColumnIdIndex2 === -1) { + return 0; + } else { + return displayedColumnIdIndex1 - displayedColumnIdIndex2; + } + }); + + this.#changeDetectorRef.markForCheck(); + } + } + + #updateDisplayedItems(): void { + this.displayedItems = this.#filterItems(this.#searchItems(this.items)); + + if (this.#dataState.onlyShowSelected) { + this.displayedItems = this.displayedItems.filter((item) => item.selected); + } + + if (this.displayedItems.length > 0) { + this.#gridApi?.hideOverlay(); + } else { + this.#gridApi?.showNoRowsOverlay(); + } + + this.#dataManagerSvc.updateDataSummary( + { + totalItems: this.items.length, + itemsMatching: this.displayedItems.length, + }, + this.#viewId, + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/data.ts new file mode 100644 index 0000000000..6652bfbb50 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/data.ts @@ -0,0 +1,149 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.html new file mode 100644 index 0000000000..65cf68c51e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.html @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.ts new file mode 100644 index 0000000000..71f1460a78 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/example.component.ts @@ -0,0 +1,157 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyDataManagerService } from '@skyux/data-manager'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkySearchModule } from '@skyux/lookup'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { of } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; +import { InlineHelpComponent } from './inline-help.component'; + +@Component({ + selector: 'app-ag-grid-data-grid-inline-help-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [AgGridModule, SkyAgGridModule, SkySearchModule, SkyToolbarModule], +}) +export class AgGridDataGridInlineHelpExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + protected searchText = ''; + protected noRowsTemplate: string; + + #columnDefs: ColDef[] = [ + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + headerComponentParams: { + inlineHelpComponent: InlineHelpComponent, + }, + }, + ]; + + #gridApi: GridApi | undefined; + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + + constructor() { + this.noRowsTemplate = `
No results found.
`; + + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + onGridReady: this.onGridReady.bind(this), + }, + }); + + this.#changeDetectorRef.markForCheck(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#changeDetectorRef.markForCheck(); + } + + protected searchApplied(searchText: string | void): void { + if (searchText) { + this.searchText = searchText; + } else { + this.searchText = ''; + } + if (this.#gridApi) { + this.#gridApi.updateGridOptions({ quickFilterText: this.searchText }); + const displayedRowCount = this.#gridApi.getDisplayedRowCount(); + + if (displayedRowCount > 0) { + this.#gridApi.hideOverlay(); + } else { + this.#gridApi.showNoRowsOverlay(); + } + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.html new file mode 100644 index 0000000000..10cd5c16aa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.html @@ -0,0 +1,5 @@ + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.ts new file mode 100644 index 0000000000..7d1d96d4fd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/inline-help/inline-help.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridHeaderInfo } from '@skyux/ag-grid'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; + +@Component({ + selector: 'app-inline-help', + templateUrl: './inline-help.component.html', + styles: [ + ` + :host { + display: block; + } + `, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyHelpInlineModule], +}) +export class InlineHelpComponent { + protected displayName: string | undefined; + readonly #headerInfo = inject(SkyAgGridHeaderInfo); + + constructor() { + this.displayName = this.#headerInfo.displayName; + } + + protected onHelpClick(): void { + alert(`Help was clicked for ${this.displayName}.`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/data.ts new file mode 100644 index 0000000000..6652bfbb50 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/data.ts @@ -0,0 +1,149 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.html new file mode 100644 index 0000000000..ed7d57ad63 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.html @@ -0,0 +1,10 @@ + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.ts new file mode 100644 index 0000000000..282e315c06 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/paging/example.component.ts @@ -0,0 +1,163 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyDataManagerService } from '@skyux/data-manager'; +import { SkyPagingModule } from '@skyux/lists'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { Subscription } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-ag-grid-data-grid-paging-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [AgGridModule, SkyAgGridModule, SkyPagingModule], +}) +export class AgGridDataGridPagingExampleComponent implements OnInit, OnDestroy { + protected currentPage = 1; + + protected readonly pageSize = 3; + + #columnDefs: ColDef[] = [ + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + ]; + + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + + #gridApi: GridApi | undefined; + #subscriptions = new Subscription(); + + readonly #activatedRoute = inject(ActivatedRoute); + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #router = inject(Router); + + constructor() { + const gridOptions: GridOptions = { + columnDefs: this.#columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + rowSelection: { mode: 'singleRow' }, + pagination: true, + suppressPaginationPanel: true, + paginationPageSize: this.pageSize, + }; + + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions, + }); + } + + public ngOnInit(): void { + this.#subscriptions.add( + this.#activatedRoute.queryParamMap + .pipe(map((params) => params.get('page') ?? '1')) + .subscribe((page) => { + this.currentPage = Number(page); + this.#gridApi?.paginationGoToPage(this.currentPage - 1); + this.#changeDetectorRef.detectChanges(); + }), + ); + + this.#subscriptions.add( + this.#router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(() => { + const page = this.#activatedRoute.snapshot.paramMap.get('page'); + + if (page) { + this.currentPage = Number(page); + } + + this.#gridApi?.paginationGoToPage(this.currentPage - 1); + this.#changeDetectorRef.detectChanges(); + }), + ); + } + + public ngOnDestroy(): void { + this.#subscriptions.unsubscribe(); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + this.#gridApi.paginationGoToPage(this.currentPage - 1); + } + + protected async onPageChange(page: number): Promise { + await this.#router.navigate(['.'], { + relativeTo: this.#activatedRoute, + queryParams: { page: page.toString(10) }, + queryParamsHandling: 'merge', + }); + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/data.ts new file mode 100644 index 0000000000..d3e062c689 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/data.ts @@ -0,0 +1,58 @@ +export interface AgGridDemoRow { + name: string; + age: number; + department: string; + jobTitle: string; +} + +export const AG_GRID_DEMO_DATA = [ + { + name: 'Billy Bob', + age: 55, + department: 'Customer Support', + jobTitle: 'Customer Support Representative', + jobLevel: '🥈', + }, + { + name: 'Jane Deere', + age: 33, + department: 'Engineering', + jobTitle: 'Software Engineer', + jobLevel: '🥇', + }, + { + name: 'John Doe', + age: 38, + department: 'Engineering', + jobTitle: 'UX Designer', + }, + { + name: 'David Smith', + age: 51, + department: 'Engineering', + jobTitle: 'Software Engineer', + jobLevel: '🥉', + }, + { + name: 'Emily Johnson', + age: 41, + department: 'Marketing', + jobTitle: 'Customer Support Representative', + }, + { + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: 'Marketing', + jobTitle: 'Account Manager', + jobLevel: '🥈', + }, + { + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: 'Marketing', + jobTitle: 'Social Media Coordinator', + jobLevel: '🥇', + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.html new file mode 100644 index 0000000000..91f27d0bee --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.html @@ -0,0 +1,14 @@ + + {{ value }} + + + @if (row['jobLevel']) { + {{ + row['jobLevel'] + }} + } + {{ value }} + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.ts new file mode 100644 index 0000000000..c85904a321 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/template-ref-column/example.component.ts @@ -0,0 +1,70 @@ +import { + Component, + OnInit, + TemplateRef, + ViewChild, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; + +import { AgGridModule } from 'ag-grid-angular'; +import { GridOptions } from 'ag-grid-community'; + +import { AG_GRID_DEMO_DATA } from './data'; + +@Component({ + selector: 'app-ag-grid-data-grid-template-ref-column-example', + templateUrl: './example.component.html', + imports: [AgGridModule, SkyAgGridModule], +}) +export class AgGridDataGridTemplateRefColumnExampleComponent implements OnInit { + @ViewChild('boldColumn', { static: true }) + protected boldColumn: TemplateRef | undefined; + + @ViewChild('emphasizedColumn', { static: true }) + protected emphasizedColumn: TemplateRef | undefined; + + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions | undefined; + + readonly #agGridSvc = inject(SkyAgGridService); + + public ngOnInit(): void { + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: [ + { + field: 'name', + headerName: 'Name', + initialWidth: 150, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 80, + resizable: false, + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Template, + cellRendererParams: { + template: this.boldColumn, + }, + initialWidth: 220, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Template, + cellRendererParams: { + template: this.emphasizedColumn, + }, + initialWidth: 220, + }, + ], + }, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.html new file mode 100644 index 0000000000..4e25fab418 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.ts new file mode 100644 index 0000000000..702dc1eeab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/context-menu.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent implements ICellRendererAngularComp { + public contextMenuAriaLabel = ''; + public deleteAriaLabel = ''; + public markInactiveAriaLabel = ''; + public moreInfoAriaLabel = ''; + + #name: string | undefined; + + public agInit(params: ICellRendererParams): void { + this.#name = params.data?.name; + this.contextMenuAriaLabel = `Context menu for ${this.#name}`; + this.deleteAriaLabel = `Delete ${this.#name}`; + this.markInactiveAriaLabel = `Mark ${this.#name} inactive`; + this.moreInfoAriaLabel = `More info for ${this.#name}`; + } + + public refresh(): boolean { + return false; + } + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.#name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/data.ts new file mode 100644 index 0000000000..69167d2b8c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/data.ts @@ -0,0 +1,156 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.html new file mode 100644 index 0000000000..31c600f1d9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.html @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.ts new file mode 100644 index 0000000000..e47199a6d4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/data-grid/top-scroll/example.component.ts @@ -0,0 +1,128 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyDataManagerService } from '@skyux/data-manager'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkySearchModule } from '@skyux/lookup'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + ValueFormatterParams, +} from 'ag-grid-community'; +import { of } from 'rxjs'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-ag-grid-data-grid-top-scroll-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [SkyDataManagerService], + imports: [AgGridModule, SkyAgGridModule, SkySearchModule, SkyToolbarModule], +}) +export class AgGridDataGridTopScrollExampleComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions: GridOptions; + protected searchText = ''; + protected noRowsTemplate = `
No results found.
`; + + #columnDefs: ColDef[] = [ + { + colId: 'context', + maxWidth: 50, + sortable: false, + cellRenderer: ContextMenuComponent, + }, + { + field: 'selected', + type: SkyCellType.RowSelector, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: AgGridDemoRow) => of(`Select ${data.name}`), + }, + }, + { + field: 'name', + headerName: 'Name', + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + }, + ]; + + #gridApi: GridApi | undefined; + + readonly #agGridSvc = inject(SkyAgGridService); + + constructor() { + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + onGridReady: (gridReadyEvent): void => { + this.onGridReady(gridReadyEvent); + }, + context: { + enableTopScroll: true, + }, + }, + }); + } + + public onGridReady(gridReadyEvent: GridReadyEvent): void { + this.#gridApi = gridReadyEvent.api; + } + + protected searchApplied(searchText: string | void): void { + this.searchText = searchText ?? ''; + + if (this.#gridApi) { + this.#gridApi.updateGridOptions({ quickFilterText: this.searchText }); + const displayedRowCount = this.#gridApi.getDisplayedRowCount(); + if (displayedRowCount > 0) { + this.#gridApi.hideOverlay(); + } else { + this.#gridApi.showNoRowsOverlay(); + } + } + } + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.html b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.html new file mode 100644 index 0000000000..1eb8db9ddd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.html @@ -0,0 +1,18 @@ + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.ts new file mode 100644 index 0000000000..a5cf8bffd8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/basic/example.component.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TreeModule } from '@blackbaud/angular-tree-component'; +import { SkyAngularTreeModule } from '@skyux/angular-tree-component'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; + +@Component({ + selector: 'app-angular-tree-component-angular-tree-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyAngularTreeModule, SkyHelpInlineModule, TreeModule], +}) +export class AngularTreeComponentAngularTreeBasicExampleComponent { + protected nodes = [ + { + name: 'Animals', + isExpanded: true, + helpPopoverContent: 'Example help content for animals.', + children: [ + { + name: 'Cats', + isExpanded: true, + helpPopoverContent: 'Example help content for cats.', + helpPopoverTitle: 'What is a cat?', + children: [ + { name: 'Burmese' }, + { name: 'Persian' }, + { name: 'Tabby' }, + ], + }, + { + name: 'Dogs', + isExpanded: true, + children: [ + { + name: 'Beagle', + helpPopoverContent: 'Example help content for beagles.', + }, + { name: 'German shepherd' }, + { name: 'Labrador retriever' }, + ], + }, + ], + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.html new file mode 100644 index 0000000000..a3eafc03ed --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.html @@ -0,0 +1,20 @@ + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.ts new file mode 100644 index 0000000000..9edb05bd15 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/angular-tree-component/angular-tree/help-key/example.component.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TreeModule } from '@blackbaud/angular-tree-component'; +import { SkyAngularTreeModule } from '@skyux/angular-tree-component'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; + +@Component({ + selector: 'app-angular-tree-component-angular-tree-help-key-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyAngularTreeModule, SkyHelpInlineModule, TreeModule], +}) +export class AngularTreeComponentAngularTreeHelpKeyExampleComponent { + protected nodes = [ + { + name: 'Animals', + isExpanded: true, + helpPopoverContent: 'Example help content for animals.', + children: [ + { + name: 'Cats', + isExpanded: true, + helpPopoverContent: 'Example help content for cats.', + helpPopoverTitle: 'What is a cat?', + children: [ + { name: 'Burmese', helpKey: 'cat-burmese' }, + { name: 'Persian', helpKey: 'cat-persian' }, + { name: 'Tabby', helpKey: 'cat-tabby' }, + ], + }, + { + name: 'Dogs', + isExpanded: true, + children: [ + { + name: 'Beagle', + helpPopoverContent: 'Example help content for beagles.', + }, + { name: 'German shepherd', helpKey: 'dog-shepherd' }, + { name: 'Labrador retriever', helpKey: 'dog-lab' }, + ], + }, + ], + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.html b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.html new file mode 100644 index 0000000000..305b2927b1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.html @@ -0,0 +1,9 @@ +
+ + + +
diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.ts b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.ts new file mode 100644 index 0000000000..25b314d500 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/currency/example.component.ts @@ -0,0 +1,38 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, +} from '@skyux/autonumeric'; +import { SkyInputBoxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-autonumeric-currency-example', + templateUrl: './example.component.html', + imports: [ReactiveFormsModule, SkyAutonumericModule, SkyInputBoxModule], +}) +export class AutonumericCurrencyExampleComponent { + protected autonumericOptions: SkyAutonumericOptions | undefined; + + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + donationAmount: new FormControl(1234.5678, [Validators.required]), + }); + + this.autonumericOptions = { + currencySymbol: ' €', + currencySymbolPlacement: 's', + decimalPlaces: 2, + decimalCharacter: ',', + digitGroupSeparator: '', + }; + } +} diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.html b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.html new file mode 100644 index 0000000000..6e5b4c4d72 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.html @@ -0,0 +1,9 @@ +
+ + + +
diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.ts b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.ts new file mode 100644 index 0000000000..e956843594 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/international-formatting/example.component.ts @@ -0,0 +1,36 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, +} from '@skyux/autonumeric'; +import { SkyInputBoxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-autonumeric-international-formatting-example', + templateUrl: './example.component.html', + imports: [ReactiveFormsModule, SkyAutonumericModule, SkyInputBoxModule], +}) +export class AutonumericInternationalFormattingExampleComponent { + protected autonumericOptions: SkyAutonumericOptions | undefined; + + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + donationAmount: new FormControl(1234.5678, [Validators.required]), + }); + + this.autonumericOptions = { + decimalCharacter: ',', + decimalPlaces: 4, + digitGroupSeparator: '', + }; + } +} diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.html b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.html new file mode 100644 index 0000000000..75d1165377 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.html @@ -0,0 +1,16 @@ +
+ + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.ts b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.ts new file mode 100644 index 0000000000..82b9dc390f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/example.component.ts @@ -0,0 +1,44 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, + SkyAutonumericOptionsProvider, +} from '@skyux/autonumeric'; +import { SkyInputBoxModule } from '@skyux/forms'; + +import { DemoAutonumericOptionsProvider } from './options-provider'; + +@Component({ + selector: 'app-autonumeric-options-provider-example', + templateUrl: './example.component.html', + providers: [ + { + provide: SkyAutonumericOptionsProvider, + useClass: DemoAutonumericOptionsProvider, + }, + ], + imports: [ReactiveFormsModule, SkyAutonumericModule, SkyInputBoxModule], +}) +export class AutonumericOptionsProviderExampleComponent { + protected donationOptions: SkyAutonumericOptions = {}; + + protected formGroup: FormGroup; + + protected pledgeOptions: SkyAutonumericOptions = { + decimalPlaces: 0, + }; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + donationAmount: new FormControl(1234.5678, [Validators.required]), + pledgeAmount: new FormControl(2345.6789, [Validators.required]), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/options-provider.ts b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/options-provider.ts new file mode 100644 index 0000000000..f2877e4871 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/options-provider/options-provider.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { + SkyAutonumericOptions, + SkyAutonumericOptionsProvider, +} from '@skyux/autonumeric'; + +@Injectable() +export class DemoAutonumericOptionsProvider extends SkyAutonumericOptionsProvider { + constructor() { + super(); + } + + public override getConfig(): SkyAutonumericOptions { + return { + currencySymbol: ' €', + currencySymbolPlacement: 's', + decimalPlaces: 2, + decimalCharacter: ',', + digitGroupSeparator: '', + }; + } +} diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.html b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.html new file mode 100644 index 0000000000..305b2927b1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.html @@ -0,0 +1,9 @@ +
+ + + +
diff --git a/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.ts b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.ts new file mode 100644 index 0000000000..442b77f116 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/autonumeric/autonumeric/preset/example.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { + SkyAutonumericModule, + SkyAutonumericOptions, +} from '@skyux/autonumeric'; +import { SkyInputBoxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-autonumeric-preset-example', + templateUrl: './example.component.html', + imports: [ReactiveFormsModule, SkyAutonumericModule, SkyInputBoxModule], +}) +export class AutonumericPresetExampleComponent { + protected autonumericOptions: SkyAutonumericOptions = 'Chinese'; + + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + donationAmount: new FormControl(1234.5678, [Validators.required]), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.html b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.html new file mode 100644 index 0000000000..d94b1537bf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.html @@ -0,0 +1,8 @@ + diff --git a/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.spec.ts new file mode 100644 index 0000000000..410f92192c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.spec.ts @@ -0,0 +1,89 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyAvatarHarness } from '@skyux/avatar/testing'; + +import { of } from 'rxjs'; + +import { AvatarExampleComponent } from './example.component'; +import { DemoService } from './example.service'; + +describe('Basic avatar harness', () => { + let uploadAvatarSpy: jasmine.Spy; + + beforeEach(() => { + uploadAvatarSpy = jasmine + .createSpy('uploadAvatarSpy') + .and.returnValue(of(undefined)); + + TestBed.configureTestingModule({ + providers: [ + { + provide: DemoService, + useValue: { + uploadAvatar: uploadAvatarSpy, + }, + }, + ], + }); + }); + + async function setupTest(): Promise<{ + fixture: ComponentFixture; + harness: SkyAvatarHarness; + }> { + const fixture = TestBed.createComponent(AvatarExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const harness: SkyAvatarHarness = await loader.getHarness( + SkyAvatarHarness.with({ dataSkyId: 'user-profile-avatar' }), + ); + + return { fixture, harness }; + } + + function createTestFile(fileSize: number): File { + return new File(['a'.repeat(fileSize)], 'test.png', { + type: 'image/png', + }); + } + + it('should display the expected avatar image', async () => { + const { harness } = await setupTest(); + + await expectAsync(harness.getInitials()).toBeResolvedTo(undefined); + await expectAsync(harness.getSrc()).toBeResolvedTo( + 'https://imgur.com/tBiGElW.png', + ); + }); + + it('should allow the user to change the avatar', async () => { + const { harness } = await setupTest(); + + await expectAsync(harness.getCanChange()).toBeResolvedTo(true); + }); + + it('should upload a new avatar and set the avatar src', async () => { + const { harness } = await setupTest(); + + const testFile = createTestFile(100); + + await harness.dropAvatarFile(testFile, true); + + expect(uploadAvatarSpy).toHaveBeenCalledOnceWith(testFile); + + await expectAsync(harness.getSrc()).toBeResolvedTo(jasmine.any(Blob)); + }); + + it('should now allow files larger than 1,000 bytes', async () => { + const { harness } = await setupTest(); + + const maxFileSize = 1000; + + await harness.dropAvatarFile(createTestFile(maxFileSize)); + await expectAsync(harness.hasMaxSizeError()).toBeResolvedTo(false); + + await harness.dropAvatarFile(createTestFile(maxFileSize + 1)); + await expectAsync(harness.hasMaxSizeError()).toBeResolvedTo(true); + + await harness.closeError(); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.ts b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.ts new file mode 100644 index 0000000000..d94392b230 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.component.ts @@ -0,0 +1,31 @@ +import { Component, inject, signal } from '@angular/core'; +import { SkyAvatarModule } from '@skyux/avatar'; +import { SkyFileItem } from '@skyux/forms'; + +import { DemoService } from './example.service'; + +@Component({ + selector: 'app-avatar-example', + templateUrl: './example.component.html', + imports: [SkyAvatarModule], +}) +export class AvatarExampleComponent { + #exampleSvc = inject(DemoService); + + protected readonly avatar = signal( + 'https://imgur.com/tBiGElW.png', + ); + protected readonly name = signal('Robert C. Hernandez'); + + protected onAvatarChanged(fileItem: SkyFileItem): void { + /** + * This is where you might upload the new avatar, + * but for this example we'll just update it locally. + */ + if (fileItem) { + this.#exampleSvc.uploadAvatar(fileItem.file).subscribe(() => { + this.avatar.set(fileItem.file); + }); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/avatar/avatar/example.service.ts b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.service.ts new file mode 100644 index 0000000000..27864f72a4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/avatar/avatar/example.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; + +import { Observable, delay, of } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public uploadAvatar(_file: File): Observable { + // Simulate uploading the file to a web service. + return of(undefined).pipe(delay(500)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.html b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.html new file mode 100644 index 0000000000..92b67b4222 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.html @@ -0,0 +1,25 @@ +
+ + + @if (favoriteColor.errors?.['opaque']) { + + } + + + +
diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.spec.ts new file mode 100644 index 0000000000..f129032067 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.spec.ts @@ -0,0 +1,58 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyColorpickerHarness } from '@skyux/colorpicker/testing'; + +import { ColorpickerBasicExampleComponent } from './example.component'; + +describe('Basic colorpicker example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyColorpickerHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(ColorpickerBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyColorpickerHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ColorpickerBasicExampleComponent], + }); + }); + + it('should have the initial values set', async () => { + const { harness } = await setupTest({ dataSkyId: 'favorite-color' }); + + await expectAsync(harness.getLabelText()).toBeResolvedTo( + 'What is your favorite color?', + ); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Pick a color with at least 80% opacity.', + ); + }); + + it('should throw an error if a low opacity color is selected', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'favorite-color', + }); + + await harness.clickColorpickerButton(); + await expectAsync(harness.isColorpickerOpen()).toBeResolvedTo(true); + + const dropdown = await harness.getColorpickerDropdown(); + + await dropdown.setAlphaValue('.2'); + await dropdown.clickApplyButton(); + fixture.detectChanges(); + + await expectAsync(harness.hasError('opaque')).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.ts new file mode 100644 index 0000000000..09749348cc --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/basic/example.component.ts @@ -0,0 +1,67 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyColorpickerModule, SkyColorpickerOutput } from '@skyux/colorpicker'; + +interface DemoForm { + favoriteColor: FormControl; +} + +function isColorpickerOutput(value: unknown): value is SkyColorpickerOutput { + return !!(value && typeof value === 'object' && 'rgba' in value); +} + +@Component({ + selector: 'app-colorpicker-basic-example', + templateUrl: './example.component.html', + imports: [ReactiveFormsModule, SkyColorpickerModule], +}) +export class ColorpickerBasicExampleComponent { + protected favoriteColor: FormControl; + protected formGroup: FormGroup; + + protected swatches: string[] = [ + '#BD4040', + '#617FC2', + '#60AC68', + '#3486BA', + '#E87134', + '#DA9C9C', + ]; + + constructor() { + this.favoriteColor = new FormControl('#f00', { + nonNullable: true, + validators: [ + (control): ValidationErrors | null => { + return isColorpickerOutput(control.value) && + control.value.rgba.alpha < 0.8 + ? { opaque: true } + : null; + }, + ], + }); + + this.formGroup = inject(FormBuilder).group({ + favoriteColor: this.favoriteColor, + }); + } + + protected onSelectedColorChanged(args: SkyColorpickerOutput): void { + console.log('Reactive form color changed:', args); + } + + protected submit(): void { + const controlValue = this.favoriteColor.value; + const favoriteColor = isColorpickerOutput(controlValue) + ? controlValue.hex + : controlValue; + + alert('Your favorite color is: \n' + favoriteColor); + } +} diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.html new file mode 100644 index 0000000000..8f09ffaefe --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.html @@ -0,0 +1,26 @@ +
+ + + @if (favoriteColor.errors?.['opaque']) { + + } + + + +
diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.spec.ts new file mode 100644 index 0000000000..cd3bca29ce --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.spec.ts @@ -0,0 +1,74 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyColorpickerHarness } from '@skyux/colorpicker/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; + +import { ColorpickerHelpKeyExampleComponent } from './example.component'; + +describe('Basic colorpicker example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyColorpickerHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent(ColorpickerHelpKeyExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const helpController = TestBed.inject(SkyHelpTestingController); + + const harness = await loader.getHarness( + SkyColorpickerHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ColorpickerHelpKeyExampleComponent, SkyHelpTestingModule], + }); + }); + + it('should have the initial values set', async () => { + const { harness } = await setupTest({ dataSkyId: 'favorite-color' }); + + await expectAsync(harness.getLabelText()).toBeResolvedTo( + 'What is your favorite color?', + ); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Pick a color with at least 80% opacity.', + ); + }); + + it('should throw an error if a low opacity color is selected', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'favorite-color', + }); + + await harness.clickColorpickerButton(); + await expectAsync(harness.isColorpickerOpen()).toBeResolvedTo(true); + + const dropdown = await harness.getColorpickerDropdown(); + + await dropdown.setAlphaValue('.2'); + await dropdown.clickApplyButton(); + fixture.detectChanges(); + + await expectAsync(harness.hasError('opaque')).toBeResolvedTo(true); + }); + + it('should have the correct help key', async () => { + const { harness, helpController } = await setupTest({ + dataSkyId: 'favorite-color', + }); + + await harness.clickHelpInline(); + + helpController.expectCurrentHelpKey('color-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.ts new file mode 100644 index 0000000000..59ce085616 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/help-key/example.component.ts @@ -0,0 +1,67 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyColorpickerModule, SkyColorpickerOutput } from '@skyux/colorpicker'; + +interface DemoForm { + favoriteColor: FormControl; +} + +function isColorpickerOutput(value: unknown): value is SkyColorpickerOutput { + return !!(value && typeof value === 'object' && 'rgba' in value); +} + +@Component({ + selector: 'app-colorpicker-help-key-example', + templateUrl: './example.component.html', + imports: [ReactiveFormsModule, SkyColorpickerModule], +}) +export class ColorpickerHelpKeyExampleComponent { + protected favoriteColor: FormControl; + protected formGroup: FormGroup; + + protected swatches: string[] = [ + '#BD4040', + '#617FC2', + '#60AC68', + '#3486BA', + '#E87134', + '#DA9C9C', + ]; + + constructor() { + this.favoriteColor = new FormControl('#f00', { + nonNullable: true, + validators: [ + (control): ValidationErrors | null => { + return isColorpickerOutput(control.value) && + control.value.rgba.alpha < 0.8 + ? { opaque: true } + : null; + }, + ], + }); + + this.formGroup = inject(FormBuilder).group({ + favoriteColor: this.favoriteColor, + }); + } + + protected onSelectedColorChanged(args: SkyColorpickerOutput): void { + console.log('Reactive form color changed:', args); + } + + protected submit(): void { + const controlValue = this.favoriteColor.value; + const favoriteColor = isColorpickerOutput(controlValue) + ? controlValue.hex + : controlValue; + + alert('Your favorite color is: \n' + favoriteColor); + } +} diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.html b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.html new file mode 100644 index 0000000000..501b0d1da0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.html @@ -0,0 +1,46 @@ +
+ + + @if (favoriteColor.errors?.['opaque']) { + + } + +
+ + + + + + diff --git a/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.ts b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.ts new file mode 100644 index 0000000000..838d0433e2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/colorpicker/colorpicker/programmatic/example.component.ts @@ -0,0 +1,71 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { + SkyColorpickerMessage, + SkyColorpickerMessageType, + SkyColorpickerModule, + SkyColorpickerOutput, +} from '@skyux/colorpicker'; + +import { Subject } from 'rxjs'; + +interface DemoForm { + favoriteColor: FormControl; +} + +function isColorpickerOutput(value: unknown): value is SkyColorpickerOutput { + return !!(value && typeof value === 'object' && 'rgba' in value); +} + +@Component({ + selector: 'app-colorpicker-programmatic-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyColorpickerModule], +}) +export class ColorpickerProgrammaticExampleComponent { + protected colorpickerController = new Subject(); + protected favoriteColor: FormControl; + protected formGroup: FormGroup; + protected showResetButton = false; + + constructor() { + this.favoriteColor = new FormControl('#f00', { + nonNullable: true, + validators: [ + (control): ValidationErrors | null => { + return isColorpickerOutput(control.value) && + control.value.rgba.alpha < 0.8 + ? { opaque: true } + : null; + }, + ], + }); + + this.formGroup = inject(FormBuilder).group({ + favoriteColor: this.favoriteColor, + }); + } + + protected openColorpicker(): void { + this.#sendMessage(SkyColorpickerMessageType.Open); + } + + protected resetColorpicker(): void { + this.#sendMessage(SkyColorpickerMessageType.Reset); + } + + protected toggleResetButton(): void { + this.#sendMessage(SkyColorpickerMessageType.ToggleResetButton); + } + + #sendMessage(type: SkyColorpickerMessageType): void { + this.colorpickerController.next({ type }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/core/id/example.component.html b/libs/components/code-examples/src/lib/modules/core/id/example.component.html new file mode 100644 index 0000000000..f95fcb8218 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/id/example.component.html @@ -0,0 +1 @@ +
This DIV's id is '{{ myDiv.id }}'
diff --git a/libs/components/code-examples/src/lib/modules/core/id/example.component.ts b/libs/components/code-examples/src/lib/modules/core/id/example.component.ts new file mode 100644 index 0000000000..ba5d421b06 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/id/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyIdModule } from '@skyux/core'; + +@Component({ + selector: 'app-core-id-example', + templateUrl: './example.component.html', + imports: [SkyIdModule], +}) +export class CoreIdExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.html b/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.html new file mode 100644 index 0000000000..71092475b1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.html @@ -0,0 +1,21 @@ +

Current media breakpoint: {{ breakpoint() }}

+ +@if (breakpoint() === 'xs') { + +} @else { + +} diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.spec.ts new file mode 100644 index 0000000000..ae2a0efa1e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyMediaQueryTestingController, + provideSkyMediaQueryTesting, +} from '@skyux/core/testing'; + +import { CoreMediaQueryBasicExampleComponent } from './example.component'; + +describe('Media query example', () => { + function setupTest(): { + fixture: ComponentFixture; + mediaController: SkyMediaQueryTestingController; + } { + const fixture = TestBed.createComponent( + CoreMediaQueryBasicExampleComponent, + ); + const mediaController = TestBed.inject(SkyMediaQueryTestingController); + + return { fixture, mediaController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideSkyMediaQueryTesting()], + }); + }); + + it('should change the breakpoint', () => { + const { fixture, mediaController } = setupTest(); + + const el = fixture.nativeElement as HTMLElement; + + mediaController.setBreakpoint('xs'); + fixture.detectChanges(); + + expect(el.querySelector('.my-nav-mobile')).toBeTruthy(); + + mediaController.setBreakpoint('lg'); + fixture.detectChanges(); + + expect(el.querySelector('.my-nav-desktop')).toBeTruthy(); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.ts new file mode 100644 index 0000000000..702b884f61 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/basic/example.component.ts @@ -0,0 +1,16 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { SkyMediaQueryService } from '@skyux/core'; +import { SkyIconModule } from '@skyux/icon'; + +@Component({ + imports: [CommonModule, SkyIconModule], + selector: 'app-core-media-query-basic-example', + templateUrl: './example.component.html', +}) +export class CoreMediaQueryBasicExampleComponent { + protected breakpoint = toSignal( + inject(SkyMediaQueryService).breakpointChange, + ); +} diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/child.component.ts b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/child.component.ts new file mode 100644 index 0000000000..ef93b7553d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/child.component.ts @@ -0,0 +1,14 @@ +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { SkyMediaQueryService } from '@skyux/core'; + +@Component({ + selector: 'app-child', + standalone: true, + template: `

Breakpoint for child: {{ breakpoint() }}

`, +}) +export class DemoChildComponent { + protected breakpoint = toSignal( + inject(SkyMediaQueryService).breakpointChange, + ); +} diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/container.component.ts b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/container.component.ts new file mode 100644 index 0000000000..4a4402525a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/container.component.ts @@ -0,0 +1,26 @@ +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { SkyMediaQueryService, SkyResponsiveHostDirective } from '@skyux/core'; + +@Component({ + hostDirectives: [SkyResponsiveHostDirective], + selector: 'app-container', + standalone: true, + styles: ` + :host { + border: 1px solid var(--sky-border-color-neutral-medium-dark); + display: block; + margin: 0 auto; + max-width: 800px; + } + `, + template: ` +

Breakpoint within container: {{ breakpoint() }}

+ + `, +}) +export class DemoContainerComponent { + protected breakpoint = toSignal( + inject(SkyMediaQueryService).breakpointChange, + ); +} diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.html b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.html new file mode 100644 index 0000000000..9f024b74f5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.html @@ -0,0 +1,33 @@ +

Viewport breakpoint: {{ breakpoint() }}

+ +

Creating a responsive host using the host directive

+ + + +

Creating a responsive host using the directive attribute

+ +
+

Breakpoint within container: {{ hostRef.breakpointChange | async }}

+
+ +

Creating a responsive host that wraps embedded views

+ +

+ The responsive host's injector must be passed to the embedded view manually. +

+ + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.scss b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.scss new file mode 100644 index 0000000000..4ae2c61153 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.scss @@ -0,0 +1,5 @@ +.my-container { + border: 1px solid var(--sky-border-color-neutral-medium-dark); + margin: 0 auto; + max-width: 500px; +} diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.spec.ts new file mode 100644 index 0000000000..1d243b45ab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.spec.ts @@ -0,0 +1,61 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyMediaQueryTestingController, + provideSkyMediaQueryTesting, +} from '@skyux/core/testing'; + +import { CoreMediaQueryResponsiveHostExampleComponent } from './example.component'; + +describe('Media query example', () => { + function setupTest(): { + fixture: ComponentFixture; + mediaController: SkyMediaQueryTestingController; + } { + const fixture = TestBed.createComponent( + CoreMediaQueryResponsiveHostExampleComponent, + ); + const mediaController = TestBed.inject(SkyMediaQueryTestingController); + + return { fixture, mediaController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideSkyMediaQueryTesting()], + }); + }); + + it('should change the breakpoint', () => { + const { fixture, mediaController } = setupTest(); + + const el = fixture.nativeElement as HTMLElement; + + const containerWithHostDirective = el.querySelector( + '[data-sky-id="container-w-host-directive"]', + ); + + const containerWithAttrDirective = el.querySelector( + '[data-sky-id="container-w-attr"]', + ); + + mediaController.setBreakpoint('xs'); + fixture.detectChanges(); + + expect(containerWithHostDirective).toHaveClass( + 'sky-responsive-container-xs', + ); + expect(containerWithAttrDirective).toHaveClass( + 'sky-responsive-container-xs', + ); + + mediaController.setBreakpoint('lg'); + fixture.detectChanges(); + + expect(containerWithHostDirective).toHaveClass( + 'sky-responsive-container-lg', + ); + expect(containerWithAttrDirective).toHaveClass( + 'sky-responsive-container-lg', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.ts b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.ts new file mode 100644 index 0000000000..18f081cadf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/media-query/responsive-host/example.component.ts @@ -0,0 +1,26 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { SkyMediaQueryService, SkyResponsiveHostDirective } from '@skyux/core'; +import { SkyIconModule } from '@skyux/icon'; + +import { DemoChildComponent } from './child.component'; +import { DemoContainerComponent } from './container.component'; + +@Component({ + imports: [ + CommonModule, + DemoChildComponent, + DemoContainerComponent, + SkyResponsiveHostDirective, + SkyIconModule, + ], + selector: 'app-core-media-query-responsive-host-example', + styleUrl: './example.component.scss', + templateUrl: './example.component.html', +}) +export class CoreMediaQueryResponsiveHostExampleComponent { + protected breakpoint = toSignal( + inject(SkyMediaQueryService).breakpointChange, + ); +} diff --git a/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.html b/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.html new file mode 100644 index 0000000000..78233b5f52 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.html @@ -0,0 +1,18 @@ + + + Default setup + + + {{ defaultValue | skyNumeric }} + + + + + With options + + + {{ configuredValue | skyNumeric: numericOptions }} + + + + diff --git a/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.spec.ts new file mode 100644 index 0000000000..aa4cfb7445 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.spec.ts @@ -0,0 +1,65 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyNumericOptions } from '@skyux/core'; + +import { CoreNumericBasicExampleComponent } from './example.component'; + +describe('Basic numeric options', () => { + async function setupTest(options?: { + defaultValue?: number; + configuredValue?: number; + config?: SkyNumericOptions; + }): Promise<{ fixture: ComponentFixture }> { + const fixture = TestBed.createComponent(CoreNumericBasicExampleComponent); + + if (options?.defaultValue !== undefined) { + fixture.componentInstance.defaultValue = options.defaultValue; + } + + if (options?.configuredValue !== undefined) { + fixture.componentInstance.configuredValue = options.configuredValue; + } + + if (options?.config !== undefined) { + fixture.componentInstance.numericOptions = options.config; + } + + fixture.detectChanges(); + await fixture.whenStable(); + + return { fixture }; + } + + function getTextContent( + fixture: ComponentFixture, + selector: string, + ): string { + const el = ( + fixture.nativeElement as HTMLElement + ).querySelector(selector); + + return el?.textContent?.trim() ?? ''; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CoreNumericBasicExampleComponent], + }); + }); + + it('should show the expected number in the default format', async () => { + const { fixture } = await setupTest({ defaultValue: 123456 }); + + fixture.detectChanges(); + + expect(getTextContent(fixture, '.default-value')).toEqual('123.5K'); + }); + + it('should show the expected number in a specified format', async () => { + const { fixture } = await setupTest({ + configuredValue: 5000000, + config: { truncate: false }, + }); + + expect(getTextContent(fixture, '.configured-value')).toEqual('5,000,000'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.ts new file mode 100644 index 0000000000..a0bdbd3d89 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/core/numeric/basic/example.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { SkyNumericModule, SkyNumericOptions } from '@skyux/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-core-numeric-basic-example', + templateUrl: './example.component.html', + imports: [SkyDescriptionListModule, SkyNumericModule], +}) +export class CoreNumericBasicExampleComponent { + @Input() + public defaultValue = 1000000; + + @Input() + public configuredValue = 1234567; + + @Input() + public numericOptions: SkyNumericOptions = { + digits: 3, + format: 'currency', + iso: 'JPY', + }; +} diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/data.ts b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/data.ts new file mode 100644 index 0000000000..0e213e5ff4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/data.ts @@ -0,0 +1,57 @@ +export interface DataManagerDemoRow { + selected?: boolean; + id: string; + name: string; + description: string; + type: string; + color: string; +} + +export const DATA_MANAGER_DEMO_DATA: DataManagerDemoRow[] = [ + { + id: '1', + name: 'Orange', + description: 'A round, orange fruit. A great source of vitamin C.', + type: 'citrus', + color: 'orange', + }, + { + id: '2', + name: 'Mango', + description: + "Very difficult to peel. Delicious in smoothies, but don't eat the skin.", + type: 'other', + color: 'orange', + }, + { + id: '3', + name: 'Lime', + description: 'A sour, green fruit used in many drinks. It grows on trees.', + type: 'citrus', + color: 'green', + }, + { + id: '4', + name: 'Strawberry', + description: + 'A red fruit that goes well with shortcake. It is the name of both the fruit and the plant!', + type: 'berry', + color: 'red', + }, + { + id: '5', + name: 'Blueberry', + description: + 'A small, blue fruit often found in muffins. When not ripe, they can be sour.', + type: 'berry', + color: 'blue', + }, + { + id: '6', + name: 'Banana', + description: + 'A yellow fruit with a thick skin. Monkeys love them, and in some countries it is customary to eat the peel.', + type: 'other', + color: 'yellow', + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.html b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.html new file mode 100644 index 0000000000..913a48c369 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.ts new file mode 100644 index 0000000000..e9830e1978 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/example.component.ts @@ -0,0 +1,68 @@ +import { + ChangeDetectionStrategy, + Component, + OnInit, + inject, +} from '@angular/core'; +import { SkyUIConfigService } from '@skyux/core'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, +} from '@skyux/data-manager'; + +import { DATA_MANAGER_DEMO_DATA, DataManagerDemoRow } from './data'; +import { FilterModalComponent } from './filter-modal.component'; +import { Filters } from './filters'; +import { ViewGridComponent } from './view-grid.component'; +import { ViewRepeaterComponent } from './view-repeater.component'; + +@Component({ + selector: 'app-data-manager-basic-example', + templateUrl: './example.component.html', + providers: [SkyDataManagerService, SkyUIConfigService], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDataManagerModule, ViewGridComponent, ViewRepeaterComponent], +}) +export class DataManagerBasicExampleComponent implements OnInit { + protected items: DataManagerDemoRow[] = DATA_MANAGER_DEMO_DATA; + + readonly #dataManagerSvc = inject(SkyDataManagerService); + + public ngOnInit(): void { + this.#dataManagerSvc.initDataManager({ + activeViewId: 'repeaterView', + dataManagerConfig: { + filterModalComponent: FilterModalComponent, + sortOptions: [ + { + id: 'az', + label: 'Name (A - Z)', + descending: false, + propertyName: 'name', + }, + { + id: 'za', + label: 'Name (Z - A)', + descending: true, + propertyName: 'name', + }, + ], + }, + defaultDataState: new SkyDataManagerState({ + filterData: { + filtersApplied: true, + filters: { + hideOrange: true, + } satisfies Filters, + }, + views: [ + { + viewId: 'gridView', + displayedColumnIds: ['selected', 'name', 'description'], + }, + ], + }), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.html b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.html new file mode 100644 index 0000000000..295ce2df6d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.ts new file mode 100644 index 0000000000..f164a0f8f2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filter-modal.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + SkyDataManagerFilterData, + SkyDataManagerFilterModalContext, +} from '@skyux/data-manager'; +import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { Filters } from './filters'; + +@Component({ + selector: 'app-filter-modal', + templateUrl: './filter-modal.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyCheckboxModule, + SkyInputBoxModule, + SkyModalModule, + ], +}) +export class FilterModalComponent implements OnInit { + protected fruitType: string | undefined; + protected hideOrange: boolean | undefined; + + readonly #context = inject(SkyDataManagerFilterModalContext); + readonly #instance = inject(SkyModalInstance); + + public ngOnInit(): void { + if (this.#context.filterData?.filters) { + const filters = this.#context.filterData.filters as Filters; + + this.fruitType = filters.type ?? 'any'; + this.hideOrange = filters.hideOrange ?? false; + } + } + + protected applyFilters(): void { + const result: SkyDataManagerFilterData = {}; + + result.filtersApplied = this.fruitType !== 'any' || this.hideOrange; + result.filters = { + type: this.fruitType, + hideOrange: this.hideOrange, + } satisfies Filters; + + this.#instance.save(result); + } + + protected clearAllFilters(): void { + this.hideOrange = false; + this.fruitType = 'any'; + } + + protected cancel(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filters.ts b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filters.ts new file mode 100644 index 0000000000..a4e9ab9c2d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/filters.ts @@ -0,0 +1,4 @@ +export interface Filters { + hideOrange?: boolean; + type?: string; +} diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.html b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.html new file mode 100644 index 0000000000..928f77c9dd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.html @@ -0,0 +1,12 @@ + + @if (isActive && isGridInitialized) { + + + + } + diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.ts b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.ts new file mode 100644 index 0000000000..a504acc497 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-grid.component.ts @@ -0,0 +1,260 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, +} from '@skyux/data-manager'; + +import { AgGridModule } from 'ag-grid-angular'; +import { + ColDef, + GridApi, + GridOptions, + GridReadyEvent, + RowSelectedEvent, +} from 'ag-grid-community'; +import { Subject, of } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { DataManagerDemoRow } from './data'; +import { Filters } from './filters'; + +@Component({ + selector: 'app-view-grid', + templateUrl: './view-grid.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule], +}) +export class ViewGridComponent implements OnInit, OnDestroy { + @Input() + public items: DataManagerDemoRow[] = []; + + protected displayedItems: DataManagerDemoRow[] = []; + protected gridOptions: GridOptions; + protected isActive = false; + protected isGridInitialized = false; + protected noRowsTemplate = `
No results found.
`; + + protected readonly viewId = 'gridView'; + + #columnDefs: ColDef[] = [ + { + colId: 'selected', + field: 'selected', + headerName: '', + maxWidth: 50, + type: SkyCellType.RowSelector, + suppressMovable: true, + lockPosition: true, + lockVisible: true, + cellRendererParams: { + // Could be a SkyAppResourcesService.getString call that returns an observable. + label: (data: DataManagerDemoRow) => of(`Select ${data.name}`), + }, + }, + { + colId: 'name', + field: 'name', + headerName: 'Fruit name', + width: 150, + }, + { + colId: 'description', + field: 'description', + headerName: 'Description', + }, + ]; + + #dataState = new SkyDataManagerState({}); + #gridApi: GridApi | undefined; + #ngUnsubscribe = new Subject(); + + #viewConfig: SkyDataViewConfig = { + id: this.viewId, + name: 'Grid View', + icon: 'table', + searchEnabled: true, + multiselectToolbarEnabled: true, + columnPickerEnabled: true, + filterButtonEnabled: true, + columnOptions: [ + { + id: 'selected', + alwaysDisplayed: true, + label: 'selected', + }, + { + id: 'name', + label: 'Fruit name', + description: 'The name of the fruit.', + }, + { + id: 'description', + label: 'Description', + description: 'Some information about the fruit.', + }, + ], + }; + + readonly #agGridSvc = inject(SkyAgGridService); + readonly #changeDetector = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + constructor() { + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + onGridReady: (args) => { + this.#onGridReady(args); + }, + }, + }); + } + + public ngOnInit(): void { + this.displayedItems = this.items; + + this.#dataManagerSvc.initDataView(this.#viewConfig); + + this.#dataManagerSvc + .getDataStateUpdates(this.viewId) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((state) => { + this.#dataState = state; + this.#setInitialColumnOrder(); + this.#updateData(); + this.#changeDetector.markForCheck(); + }); + + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((id) => { + this.isActive = id === this.viewId; + this.#changeDetector.markForCheck(); + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + protected onRowSelected( + rowSelectedEvent: RowSelectedEvent, + ): void { + if (!rowSelectedEvent.data?.selected) { + this.#updateData(); + } + } + + #filterItems(items: DataManagerDemoRow[]): DataManagerDemoRow[] { + let filteredItems = items; + + const filterData = this.#dataState && this.#dataState.filterData; + + if (filterData?.filters) { + const filters = filterData.filters as Filters; + + filteredItems = items.filter((item) => { + return ( + ((filters.hideOrange && item.color !== 'orange') ?? + !filters.hideOrange) && + ((filters.type !== 'any' && item.type === filters.type) || + !filters.type || + filters.type === 'any') + ); + }); + } + + return filteredItems; + } + + #onGridReady(event: GridReadyEvent): void { + this.#gridApi = event.api; + this.#updateData(); + } + + #searchItems(items: DataManagerDemoRow[]): DataManagerDemoRow[] { + let searchedItems = items; + const searchText = this.#dataState && this.#dataState.searchText; + + if (searchText) { + searchedItems = items.filter((item: DataManagerDemoRow) => { + let property: keyof typeof item; + + for (property in item) { + if ( + Object.prototype.hasOwnProperty.call(item, property) && + (property === 'name' || property === 'description') + ) { + const propertyText = item[property].toLowerCase(); + if (propertyText.includes(searchText)) { + return true; + } + } + } + + return false; + }); + } + return searchedItems; + } + + #setInitialColumnOrder(): void { + const viewState = this.#dataState.getViewStateById(this.viewId); + const visibleColumns = viewState?.displayedColumnIds ?? []; + + this.#columnDefs.sort((col1, col2) => { + const col1Index = visibleColumns.findIndex( + (colId: string) => colId === col1.colId, + ); + const col2Index = visibleColumns.findIndex( + (colId: string) => colId === col2.colId, + ); + + if (col1Index === -1) { + col1.hide = true; + return 0; + } else if (col2Index === -1) { + col2.hide = true; + return 0; + } else { + return col1Index - col2Index; + } + }); + + this.isGridInitialized = true; + } + + #updateData(): void { + this.displayedItems = this.#filterItems(this.#searchItems(this.items)); + + if (this.#dataState.onlyShowSelected) { + this.displayedItems = this.displayedItems.filter((item) => item.selected); + } + + if (this.displayedItems.length > 0) { + this.#gridApi?.hideOverlay(); + } else { + this.#gridApi?.showNoRowsOverlay(); + } + + this.#dataManagerSvc.updateDataSummary( + { + totalItems: this.items.length, + itemsMatching: this.displayedItems.length, + }, + this.viewId, + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.html b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.html new file mode 100644 index 0000000000..aa46693da3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.html @@ -0,0 +1,20 @@ + + @if (isActive) { + + @for (item of displayedItems; track item) { + + + {{ item.name }} + + + {{ item.description }} + + + } + + } + diff --git a/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.ts b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.ts new file mode 100644 index 0000000000..2cf869a91d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/data-manager/data-manager/basic/view-repeater.component.ts @@ -0,0 +1,218 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, +} from '@skyux/data-manager'; +import { SkyRepeaterModule } from '@skyux/lists'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { DataManagerDemoRow } from './data'; +import { Filters } from './filters'; + +@Component({ + selector: 'app-view-repeater', + templateUrl: './view-repeater.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDataManagerModule, SkyRepeaterModule], +}) +export class ViewRepeaterComponent implements OnInit, OnDestroy { + @Input() + public items: DataManagerDemoRow[] = []; + + protected displayedItems: DataManagerDemoRow[] = []; + protected isActive = false; + + protected readonly viewId = 'repeaterView'; + + #dataState = new SkyDataManagerState({}); + #ngUnsubscribe = new Subject(); + + #viewConfig: SkyDataViewConfig = { + id: this.viewId, + name: 'Repeater View', + icon: 'list', + searchEnabled: true, + sortEnabled: true, + filterButtonEnabled: true, + multiselectToolbarEnabled: true, + onClearAllClick: () => { + this.#clearAll(); + }, + onSelectAllClick: () => { + this.#selectAll(); + }, + }; + + readonly #changeDetector = inject(ChangeDetectorRef); + readonly #dataManagerSvc = inject(SkyDataManagerService); + + public ngOnInit(): void { + this.displayedItems = this.items; + + this.#dataManagerSvc.initDataView(this.#viewConfig); + + this.#dataManagerSvc + .getDataStateUpdates(this.viewId) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((state) => { + this.#dataState = state; + this.#updateData(); + }); + + this.#dataManagerSvc + .getActiveViewIdUpdates() + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((id) => { + this.isActive = id === this.viewId; + this.#changeDetector.markForCheck(); + }); + } + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + protected onItemSelect(isSelected: boolean, item: DataManagerDemoRow): void { + const selectedItems = this.#dataState.selectedIds ?? []; + const itemIndex = selectedItems.indexOf(item.id); + + if (isSelected && itemIndex === -1) { + selectedItems.push(item.id); + } else if (!isSelected && itemIndex !== -1) { + selectedItems.splice(itemIndex, 1); + } + + this.#dataState.selectedIds = selectedItems; + this.#dataManagerSvc.updateDataState(this.#dataState, this.viewId); + + if (this.#dataState.onlyShowSelected && this.displayedItems) { + this.displayedItems = this.displayedItems.filter((itm) => itm.selected); + this.#changeDetector.markForCheck(); + } + } + + #updateData(): void { + const selectedIds = this.#dataState.selectedIds ?? []; + + this.items.forEach((item) => { + item.selected = selectedIds.includes(item.id); + }); + + this.displayedItems = this.#filterItems(this.#searchItems(this.items)); + + if (this.#dataState.onlyShowSelected) { + this.displayedItems = this.displayedItems.filter((item) => item.selected); + } + + this.#dataManagerSvc.updateDataSummary( + { + totalItems: this.items.length, + itemsMatching: this.displayedItems.length, + }, + this.viewId, + ); + + this.#changeDetector.markForCheck(); + } + + #searchItems(items: DataManagerDemoRow[]): DataManagerDemoRow[] { + let searchedItems = items; + const searchText = + this.#dataState && this.#dataState.searchText?.toUpperCase(); + + if (searchText) { + searchedItems = items.filter((item: DataManagerDemoRow) => { + let property: keyof typeof item; + + for (property in item) { + if ( + Object.prototype.hasOwnProperty.call(item, property) && + (property === 'name' || property === 'description') + ) { + const propertyText = item[property].toUpperCase(); + if (propertyText.includes(searchText)) { + return true; + } + } + } + + return false; + }); + } + + return searchedItems; + } + + #filterItems(items: DataManagerDemoRow[]): DataManagerDemoRow[] { + let filteredItems = items; + const filterData = this.#dataState && this.#dataState.filterData; + + if (filterData?.filters) { + const filters = filterData.filters as Filters; + + filteredItems = items.filter((item: DataManagerDemoRow) => { + if ( + ((filters.hideOrange && item.color !== 'orange') ?? + !filters.hideOrange) && + ((filters.type !== 'any' && item.type === filters.type) || + !filters.type || + filters.type === 'any') + ) { + return true; + } + + return false; + }); + } + + return filteredItems; + } + + #selectAll(): void { + const selectedIds = this.#dataState.selectedIds ?? []; + + this.displayedItems.forEach((item) => { + if (!item.selected) { + item.selected = true; + selectedIds.push(item.id); + } + }); + + this.#dataState.selectedIds = selectedIds; + this.#dataManagerSvc.updateDataState(this.#dataState, this.viewId); + this.#changeDetector.markForCheck(); + } + + #clearAll(): void { + const selectedIds = this.#dataState.selectedIds ?? []; + + this.displayedItems.forEach((item) => { + if (item.selected) { + const itemIndex = selectedIds.indexOf(item.id); + item.selected = false; + selectedIds.splice(itemIndex, 1); + } + }); + + if (this.#dataState.onlyShowSelected) { + this.displayedItems = []; + } + + this.#dataState.selectedIds = selectedIds; + this.#dataManagerSvc.updateDataState(this.#dataState, this.viewId); + this.#changeDetector.markForCheck(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.html new file mode 100644 index 0000000000..5b0d92f95b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.html @@ -0,0 +1,3 @@ +
{{ myDate | skyDate }}
+
{{ myDate | skyDate: 'medium' }}
+
{{ myDate | skyDate: 'medium' : 'es-MX' }}
diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.ts new file mode 100644 index 0000000000..4c0983414b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-pipe/basic/example.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SkyDatePipeModule } from '@skyux/datetime'; + +@Component({ + selector: 'app-datetime-date-pipe-basic-example', + templateUrl: './example.component.html', + imports: [SkyDatePipeModule], +}) +export class DatetimeDatePipeBasicExampleComponent { + protected myDate = new Date('11/05/1955'); +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.html new file mode 100644 index 0000000000..97b2fc4f0c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.html @@ -0,0 +1,24 @@ +
+
+ + @if (lastDonation.errors?.['dateWeekend']) { + + } + +
+
+ + + By default, donations include one-time gifts, pledges, and matching gifts. To + customize, see Giving settings. + diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.spec.ts new file mode 100644 index 0000000000..669c279fe9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.spec.ts @@ -0,0 +1,63 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyDateRangeCalculatorId } from '@skyux/datetime'; +import { SkyDateRangePickerHarness } from '@skyux/datetime/testing'; + +import { DatetimeDateRangePickerBasicExampleComponent } from '../../date-range-picker/basic/example.component'; + +describe('Basic date range picker example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyDateRangePickerHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + DatetimeDateRangePickerBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyDateRangePickerHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + DatetimeDateRangePickerBasicExampleComponent, + NoopAnimationsModule, + ], + }); + }); + + it('should set initial value', async () => { + const { harness } = await setupTest({ + dataSkyId: 'last-donation', + }); + + await expectAsync(harness.getLabelText()).toBeResolvedTo('Last donation'); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Donations received today are updated at the top of each hour.', + ); + await harness.clickHelpInline(); + await expectAsync(harness.getHelpPopoverContent()).toBeResolvedTo( + 'By default, donations include one-time gifts, pledges, and matching gifts. To customize, see Giving settings.', + ); + }); + + it('should throw an error if a weekend is selected', async () => { + const { harness } = await setupTest({ + dataSkyId: 'last-donation', + }); + + await harness.selectCalculator(SkyDateRangeCalculatorId.SpecificRange); + await harness.setEndDateValue('05/22/2022'); + + await expectAsync(harness.hasError('dateWeekend')).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.ts new file mode 100644 index 0000000000..014840af98 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/basic/example.component.ts @@ -0,0 +1,71 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { + SkyDateRangeCalculation, + SkyDateRangeCalculatorId, + SkyDateRangePickerModule, + SkyDatepickerModule, +} from '@skyux/datetime'; + +function dateRangeExcludesWeekend( + control: AbstractControl, +): ValidationErrors | null { + const startDate = control.value.startDate; + const endDate = control.value.endDate; + + const isWeekend = (value: unknown): boolean => { + return ( + value instanceof Date && (value.getDay() === 6 || value.getDay() === 0) + ); + }; + + if (isWeekend(startDate) || isWeekend(endDate)) { + return { dateWeekend: true }; + } + + return null; +} + +@Component({ + selector: 'app-datetime-date-range-picker-basic-example', + templateUrl: './example.component.html', + // NOTE: `SkyDatepickerModule` is imported to address a stackblitz error. + // Consumers DO NOT need to import `SkyDatepickerModule` when using `sky-date-ranger-picker` + imports: [ + FormsModule, + ReactiveFormsModule, + SkyDateRangePickerModule, + SkyDatepickerModule, + ], +}) +export class DatetimeDateRangePickerBasicExampleComponent { + protected dateFormat: string | undefined; + protected disabled = false; + protected hintText = + 'Donations received today are updated at the top of each hour.'; + protected labelText = 'Last donation'; + + protected lastDonation = new FormControl( + { + value: { + calculatorId: SkyDateRangeCalculatorId.AnyTime, + }, + disabled: this.disabled, + }, + { + nonNullable: true, + validators: [dateRangeExcludesWeekend], + }, + ); + + protected formGroup = inject(FormBuilder).group({ + lastDonation: this.lastDonation, + }); +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.html new file mode 100644 index 0000000000..7cd59e08d7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.html @@ -0,0 +1,28 @@ +
+
+ + @if ( + lastDonation.errors?.['skyDateRange']?.errors?.['dateIsAfterToday'] + ) { + + } + +
+

+ Selected calculator: {{ selectedCalculator()?.shortDescription$ | async }} +

+
+ + + By default, donations include one-time gifts, pledges, and matching gifts. To + customize, see Giving settings. + diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.ts new file mode 100644 index 0000000000..17b1bd3864 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/custom-calculator/example.component.ts @@ -0,0 +1,108 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + FormBuilder, + FormControl, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { + SkyDateRangeCalculation, + SkyDateRangeCalculator, + SkyDateRangeCalculatorId, + SkyDateRangeCalculatorType, + SkyDateRangePickerModule, + SkyDateRangeService, +} from '@skyux/datetime'; + +@Component({ + selector: 'app-datetime-date-range-picker-custom-calculator-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyDateRangePickerModule, + ], +}) +export class DatetimeDateRangePickerCustomCalculatorExampleComponent { + readonly #dateRangeSvc = inject(SkyDateRangeService); + + protected customCalculators: SkyDateRangeCalculatorId[] | undefined; + protected disabled = false; + protected hintText = + 'Donations received today are updated at the top of each hour.'; + protected labelText = 'Last donation'; + + protected lastDonation = new FormControl( + { + value: { + calculatorId: SkyDateRangeCalculatorId.AnyTime, + }, + disabled: this.disabled, + }, + { nonNullable: true }, + ); + + protected formGroup = inject(FormBuilder).group({ + lastDonation: this.lastDonation, + }); + + protected selectedCalculator = signal( + this.#getCalculatorById(SkyDateRangeCalculatorId.AnyTime), + ); + + constructor() { + const since1999Calculator = this.#dateRangeSvc.createCalculator({ + shortDescription: 'Since 1999', + type: SkyDateRangeCalculatorType.Relative, + getValue: () => { + return { + startDate: new Date('1/1/1999'), + endDate: new Date(), + }; + }, + }); + + const dateBeforeToday = this.#dateRangeSvc.createCalculator({ + shortDescription: 'Date before today', + type: SkyDateRangeCalculatorType.Before, + validate: (value): ValidationErrors | null => { + if (value?.endDate && value.endDate > new Date()) { + return { + dateIsAfterToday: true, + }; + } + + return null; + }, + getValue: () => { + return { + endDate: new Date(), + }; + }, + }); + + this.customCalculators = [ + SkyDateRangeCalculatorId.SpecificRange, + SkyDateRangeCalculatorId.LastFiscalYear, + since1999Calculator.calculatorId, + dateBeforeToday.calculatorId, + SkyDateRangeCalculatorId.AnyTime, + ]; + + this.lastDonation.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((value) => { + const selectedCalculator = this.#getCalculatorById(value?.calculatorId); + + this.selectedCalculator.set(selectedCalculator); + }); + } + + #getCalculatorById(id: SkyDateRangeCalculatorId): SkyDateRangeCalculator { + return this.#dateRangeSvc.filterCalculators([id])[0]; + } +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.html new file mode 100644 index 0000000000..9fe6abb072 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.html @@ -0,0 +1,12 @@ +
+
+ + +
diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.spec.ts new file mode 100644 index 0000000000..d55f364344 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.spec.ts @@ -0,0 +1,64 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyDateRangePickerHarness } from '@skyux/datetime/testing'; + +import { DatetimeDateRangePickerHelpKeyExampleComponent } from './example.component'; + +describe('Basic date range picker example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyDateRangePickerHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + DatetimeDateRangePickerHelpKeyExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + const helpController = TestBed.inject(SkyHelpTestingController); + + const harness = await loader.getHarness( + SkyDateRangePickerHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + DatetimeDateRangePickerHelpKeyExampleComponent, + NoopAnimationsModule, + SkyHelpTestingModule, + ], + }); + }); + + it('should set initial value', async () => { + const { harness } = await setupTest({ + dataSkyId: 'last-donation', + }); + + await expectAsync(harness.getLabelText()).toBeResolvedTo('Last donation'); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Donations received today are updated at the top of each hour.', + ); + }); + + it('should have the correct help key', async () => { + const { harness, helpController } = await setupTest({ + dataSkyId: 'last-donation', + }); + + await harness.clickHelpInline(); + + helpController.expectCurrentHelpKey('dates-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.ts new file mode 100644 index 0000000000..889c74ba9b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/date-range-picker/help-key/example.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + SkyDateRangeCalculation, + SkyDateRangeCalculatorId, + SkyDateRangePickerModule, +} from '@skyux/datetime'; + +@Component({ + selector: 'app-datetime-date-range-picker-help-key-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyDateRangePickerModule], +}) +export class DatetimeDateRangePickerHelpKeyExampleComponent { + protected dateFormat: string | undefined; + protected disabled = false; + protected hintText = + 'Donations received today are updated at the top of each hour.'; + protected labelText = 'Last donation'; + + protected lastDonation = new FormControl( + { + value: { + calculatorId: SkyDateRangeCalculatorId.AnyTime, + }, + disabled: this.disabled, + }, + { + nonNullable: true, + }, + ); + + protected formGroup = inject(FormBuilder).group({ + lastDonation: this.lastDonation, + }); +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.html new file mode 100644 index 0000000000..1c354c0453 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.html @@ -0,0 +1,24 @@ +
+
+ + + + + @if (startDate.errors?.['invalidWeekend']) { + + } + +
+ +

+ Selected date: {{ startDate.value | json }} +

+
diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.spec.ts new file mode 100644 index 0000000000..b600b75b31 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.spec.ts @@ -0,0 +1,74 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyDatepickerHarness } from '@skyux/datetime/testing'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; + +import { DatetimeDatepickerBasicExampleComponent } from './example.component'; + +describe('Basic datepicker example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + inputHarness: SkyInputBoxHarness; + datepickerHarness: SkyDatepickerHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + DatetimeDatepickerBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const inputHarness = await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }), + ); + const datepickerHarness = + await inputHarness.queryHarness(SkyDatepickerHarness); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { inputHarness, datepickerHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DatetimeDatepickerBasicExampleComponent, NoopAnimationsModule], + }); + }); + + it('should have initial setup values', async () => { + const { inputHarness, datepickerHarness, fixture } = await setupTest({ + dataSkyId: 'datepicker-example', + }); + + await expectAsync(inputHarness.getHintText()).toBeResolvedTo( + 'Must be before your 1 year anniversary. Use the format MM/DD/YYYY.', + ); + + await datepickerHarness.clickCalendarButton(); + + await inputHarness.clickHelpInline(); + await expectAsync(inputHarness.getHelpPopoverContent()).toBeResolvedTo( + fixture.componentInstance.helpPopoverContent, + ); + + await datepickerHarness.clickCalendarButton(); + const calendarHarness = await datepickerHarness.getDatepickerCalendar(); + await expectAsync(calendarHarness.getSelectedValue()).toBeResolvedTo( + 'Friday, October 12th 2001', + ); + }); + + it('should throw an error if selecting a weekend', async () => { + const { inputHarness, datepickerHarness } = await setupTest({ + dataSkyId: 'datepicker-example', + }); + + await datepickerHarness.clickCalendarButton(); + const calendarHarness = await datepickerHarness.getDatepickerCalendar(); + + await calendarHarness.clickDate('Saturday, October 13th 2001'); + await expectAsync( + inputHarness.hasCustomFormError('invalidWeekend'), + ).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.ts new file mode 100644 index 0000000000..ce927059ce --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/basic/example.component.ts @@ -0,0 +1,64 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyDatepickerModule } from '@skyux/datetime'; +import { SkyInputBoxModule } from '@skyux/forms'; + +interface DemoForm { + startDate: FormControl; +} + +function validateDate( + control: AbstractControl, +): ValidationErrors | null { + const date = control.value; + if (date instanceof Date) { + const day = date?.getDay(); + + return day !== undefined && (day === 0 || day === 6) + ? { + invalidWeekend: true, + } + : null; + } + return null; +} + +@Component({ + selector: 'app-datetime-datepicker-basic-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyDatepickerModule, + SkyInputBoxModule, + ], +}) +export class DatetimeDatepickerBasicExampleComponent { + protected formGroup: FormGroup; + protected startDate: FormControl; + protected hintText = 'Must be before your 1 year anniversary.'; + + public helpPopoverContent = + 'If you need help with registration, choose a date at least 8 business days after you arrive. The process takes up to 7 business days from the start date.'; + + constructor() { + this.startDate = new FormControl(new Date('10/12/2001'), { + validators: [Validators.required, validateDate], + }); + + this.formGroup = inject(FormBuilder).group({ + startDate: this.startDate, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.html new file mode 100644 index 0000000000..dcf62da00e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.html @@ -0,0 +1,15 @@ +
+
+ + + + + +
+ +

+ Selected date: {{ formGroup.value.startDate | json }} +

+
diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.ts new file mode 100644 index 0000000000..0e4759781f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/custom-dates/example.component.ts @@ -0,0 +1,124 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + SkyDatepickerCalendarChange, + SkyDatepickerCustomDate, + SkyDatepickerModule, +} from '@skyux/datetime'; +import { SkyInputBoxModule } from '@skyux/forms'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +@Component({ + selector: 'app-datetime-datepicker-custom-dates-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyDatepickerModule, + SkyInputBoxModule, + ], +}) +export class DatetimeDatepickerCustomDatesExampleComponent { + protected formGroup: FormGroup; + + #formBuilder = inject(FormBuilder); + + constructor() { + this.formGroup = this.#formBuilder.group({ + startDate: new FormControl(), + }); + } + + protected onCalendarDateRangeChange( + event: SkyDatepickerCalendarChange, + ): void { + if (event) { + // Bind observable to `customDates` argument and simulate delay for async process to finish. + // Normally, `getCustomDates()` would be replaced by an async call to fetch data. + event.customDates = this.#getCustomDates(event).pipe(delay(2000)); + } + } + + /** + * Generate fake custom dates based on the date range returned from the event. + * This is for examplenstration purposes only. + */ + #getCustomDates( + event: SkyDatepickerCalendarChange, + ): Observable { + const getNextDate = function (startDate: Date, daysToAdd: number): Date { + const newDate = new Date(startDate); + newDate.setDate(newDate.getDate() + daysToAdd); + return newDate; + }; + + const customDates: SkyDatepickerCustomDate[] = []; + customDates.push({ + date: event.startDate, + disabled: false, + keyDate: true, + keyDateText: ['Onboarding meeting'], + }); + + customDates.push({ + date: getNextDate(event.startDate, 8), + disabled: false, + keyDate: true, + keyDateText: ['Department all hands'], + }); + + customDates.push({ + date: getNextDate(event.startDate, 9), + disabled: false, + keyDate: true, + keyDateText: ['Company retreat'], + }); + + customDates.push({ + date: getNextDate(event.startDate, 10), + disabled: true, + keyDate: true, + keyDateText: ['Federal holiday'], + }); + + customDates.push({ + date: getNextDate(event.startDate, 11), + disabled: true, + keyDate: false, + keyDateText: [], + }); + + customDates.push({ + date: getNextDate(event.startDate, 12), + disabled: false, + keyDate: true, + keyDateText: ['New hire review due'], + }); + + customDates.push({ + date: getNextDate(event.startDate, 13), + disabled: false, + keyDate: true, + keyDateText: ['Key note speaker', 'Focus group'], + }); + + customDates.push({ + date: event.endDate, + disabled: false, + keyDate: true, + keyDateText: ['Customer lunch and learn'], + }); + + return of(customDates); + } +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.html new file mode 100644 index 0000000000..78d7a3d79c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.html @@ -0,0 +1,23 @@ +
+
+ + + + + +
+ +

+ Selected date: {{ getFuzzyDateForDisplay }} +

+
diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.spec.ts new file mode 100644 index 0000000000..fec48b474c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.spec.ts @@ -0,0 +1,60 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyDatepickerHarness } from '@skyux/datetime/testing'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; + +import { DatetimeDatepickerFuzzyExampleComponent } from './example.component'; + +describe('Fuzzy datepicker example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + inputHarness: SkyInputBoxHarness; + datepickerHarness: SkyDatepickerHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + DatetimeDatepickerFuzzyExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const inputHarness = await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }), + ); + const datepickerHarness = + await inputHarness.queryHarness(SkyDatepickerHarness); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { inputHarness, datepickerHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DatetimeDatepickerFuzzyExampleComponent, NoopAnimationsModule], + }); + }); + + it('should have initial setup values', async () => { + const { inputHarness, datepickerHarness, fixture } = await setupTest({ + dataSkyId: 'fuzzy-datepicker-example', + }); + + await expectAsync(inputHarness.getHintText()).toBeResolvedTo( + "Include a partial date if you don't have the exact date.", + ); + + await datepickerHarness.clickCalendarButton(); + + await inputHarness.clickHelpInline(); + await expectAsync(inputHarness.getHelpPopoverContent()).toBeResolvedTo( + fixture.componentInstance.helpPopoverContent, + ); + + await datepickerHarness.clickCalendarButton(); + const calendarHarness = await datepickerHarness.getDatepickerCalendar(); + await expectAsync(calendarHarness.getSelectedValue()).toBeResolvedTo( + 'Saturday, November 5th 1955', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.ts new file mode 100644 index 0000000000..d409b87c9e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/datepicker/fuzzy/example.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyDatepickerModule } from '@skyux/datetime'; +import { SkyInputBoxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-datetime-datepicker-fuzzy-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyDatepickerModule, + SkyInputBoxModule, + ], +}) +export class DatetimeDatepickerFuzzyExampleComponent { + protected formGroup: FormGroup; + protected hintText = + "Include a partial date if you don't have the exact date."; + public helpPopoverContent = + 'Your date of birth ensures that your benefits include the supplemental at-home services for your age group.'; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + dob: new FormControl(new Date(1955, 10, 5), { + validators: Validators.required, + }), + }); + } + + protected get getFuzzyDateForDisplay(): string { + return JSON.stringify(this.formGroup.get('dob')?.value); + } +} diff --git a/libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.html b/libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.html new file mode 100644 index 0000000000..9e50d8370c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.html @@ -0,0 +1,23 @@ +
+
+ + + + + @if (time.errors?.['invalidMinute']) { + + } + +
+
diff --git a/libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.ts new file mode 100644 index 0000000000..980310926b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/datetime/timepicker/basic/example.component.ts @@ -0,0 +1,62 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyTimepickerModule, SkyTimepickerTimeOutput } from '@skyux/datetime'; +import { SkyInputBoxModule } from '@skyux/forms'; + +interface DemoForm { + time: FormControl; +} + +function isTimepickerOutput(value: unknown): value is SkyTimepickerTimeOutput { + return !!(value && typeof value === 'object' && 'minute' in value); +} + +function validateTime( + control: AbstractControl, +): ValidationErrors | null { + const minute = isTimepickerOutput(control.value) + ? control.value.minute + : undefined; + + return minute && minute % 15 !== 0 ? { invalidMinute: true } : null; +} + +@Component({ + selector: 'app-datetime-timepicker-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyTimepickerModule, + ], +}) +export class DatetimeTimepickerBasicExampleComponent { + protected formGroup: FormGroup; + protected time: FormControl; + + protected hintText = 'Choose a time that allows for late arrivals.'; + + protected helpPopoverContent = + 'Allow time to complete all activities that your team signed up for. All activities take about 30 minutes, except the ropes course, which takes 60 minutes.'; + + constructor() { + this.time = new FormControl('2:45', { + nonNullable: true, + validators: [Validators.required, validateTime], + }); + + this.formGroup = inject(FormBuilder).group({ + time: this.time, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.html b/libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.html new file mode 100644 index 0000000000..b99e834f63 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.html @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom error with a custom image. + Custom description. + + + + + + + + Custom error with a default image. + + + Custom description. + + + + + diff --git a/libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.ts b/libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.ts new file mode 100644 index 0000000000..1031ce1afa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/errors/error/embedded/example.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { SkyErrorModule } from '@skyux/errors'; + +@Component({ + selector: 'app-errors-error-embedded-example', + templateUrl: './example.component.html', + imports: [SkyErrorModule], +}) +export class ErrorsErrorEmbeddedExampleComponent { + protected customAction(): void { + alert('action clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.html b/libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.html new file mode 100644 index 0000000000..f22bad98c5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.html @@ -0,0 +1,7 @@ + diff --git a/libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.ts b/libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.ts new file mode 100644 index 0000000000..ea8aa27806 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/errors/error/modal/example.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from '@angular/core'; +import { SkyErrorModalService } from '@skyux/errors'; + +@Component({ + standalone: true, + selector: 'app-errors-error-modal-example', + templateUrl: './example.component.html', +}) +export class ErrorsErrorModalExampleComponent { + readonly #errorSvc = inject(SkyErrorModalService); + + public openErrorModal(): void { + this.#errorSvc.open({ + errorTitle: 'Something went wrong.', + errorDescription: 'Close the modal, and come back later.', + errorCloseText: 'Close', + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.html b/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.html new file mode 100644 index 0000000000..8f3879be21 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.html @@ -0,0 +1,23 @@ + + + + + diff --git a/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.ts new file mode 100644 index 0000000000..e751dcaa64 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/example.component.ts @@ -0,0 +1,48 @@ +import { Component, inject } from '@angular/core'; +import { SkyFlyoutInstance, SkyFlyoutService } from '@skyux/flyout'; + +import { FlyoutComponent } from './flyout.component'; + +@Component({ + standalone: true, + selector: 'app-flyout-basic-example', + templateUrl: './example.component.html', +}) +export class FlyoutBasicExampleComponent { + #flyout: SkyFlyoutInstance | undefined; + + readonly #flyoutSvc = inject(SkyFlyoutService); + + protected closeAndRemoveFlyout(): void { + if (this.#flyout?.isOpen) { + this.#flyoutSvc.close(); + } + + this.#flyout = undefined; + } + + protected openFlyoutWithCustomWidth(): void { + this.#flyout = this.#flyoutSvc.open(FlyoutComponent, { + ariaLabelledBy: 'flyout-title', + ariaRole: 'dialog', + defaultWidth: 350, + maxWidth: 500, + minWidth: 200, + }); + + this.#flyout.closed.subscribe(() => { + this.#flyout = undefined; + }); + } + + protected openSimpleFlyout(): void { + this.#flyout = this.#flyoutSvc.open(FlyoutComponent, { + ariaLabelledBy: 'flyout-title', + ariaRole: 'dialog', + }); + + this.#flyout.closed.subscribe(() => { + this.#flyout = undefined; + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/flyout.component.ts b/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/flyout.component.ts new file mode 100644 index 0000000000..efc82bd204 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/flyout/flyout/basic/flyout.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-flyout', + template: ` +
+

Sample flyout

+

+ Flyouts can display large quantities of supplementary information + related to a task, including: +

+
    +
  • lists
  • +
  • records
  • +
  • analytics
  • +
+
+ `, +}) +export class FlyoutComponent {} diff --git a/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.html b/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.html new file mode 100644 index 0000000000..3b8073cbd4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.html @@ -0,0 +1,23 @@ + + + + + diff --git a/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.ts b/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.ts new file mode 100644 index 0000000000..8b5995e407 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/example.component.ts @@ -0,0 +1,73 @@ +import { Component, inject } from '@angular/core'; +import { SkyFlyoutInstance, SkyFlyoutService } from '@skyux/flyout'; + +import { FlyoutComponent } from './flyout.component'; + +@Component({ + standalone: true, + selector: 'app-flyout-custom-headers-example', + templateUrl: './example.component.html', +}) +export class FlyoutCustomHeadersExampleComponent { + #flyout: SkyFlyoutInstance | undefined; + + readonly #flyoutSvc = inject(SkyFlyoutService); + + protected openFlyoutWithIterators(): void { + this.#flyout = this.#flyoutSvc.open(FlyoutComponent, { + ariaLabelledBy: 'flyout-title', + ariaRole: 'dialog', + showIterator: true, + }); + + this.#flyout.iteratorNextButtonClick.subscribe(() => { + alert('Next iterator button clicked!'); + }); + + this.#flyout.iteratorPreviousButtonClick.subscribe(() => { + alert('Previous iterator button clicked!'); + }); + + this.#flyout.closed.subscribe(() => { + this.#flyout = undefined; + }); + } + + protected openFlyoutWithRoutePermalink(): void { + this.#flyout = this.#flyoutSvc.open(FlyoutComponent, { + ariaLabelledBy: 'flyout-title', + ariaRole: 'dialog', + permalink: { + label: 'Go to Components page', + route: { + commands: ['/components'], + extras: { + fragment: 'helloWorld', + queryParams: { + foo: 'bar', + }, + }, + }, + }, + }); + + this.#flyout.closed.subscribe(() => { + this.#flyout = undefined; + }); + } + + protected openFlyoutWithUrlPermalink(): void { + this.#flyout = this.#flyoutSvc.open(FlyoutComponent, { + ariaLabelledBy: 'flyout-title', + ariaRole: 'dialog', + permalink: { + label: `Visit blackbaud.com`, + url: 'http://www.blackbaud.com', + }, + }); + + this.#flyout.closed.subscribe(() => { + this.#flyout = undefined; + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/flyout.component.ts b/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/flyout.component.ts new file mode 100644 index 0000000000..efc82bd204 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/flyout/flyout/custom-headers/flyout.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-flyout', + template: ` +
+

Sample flyout

+

+ Flyouts can display large quantities of supplementary information + related to a task, including: +

+
    +
  • lists
  • +
  • records
  • +
  • analytics
  • +
+
+ `, +}) +export class FlyoutComponent {} diff --git a/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.html b/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.html new file mode 100644 index 0000000000..7f3da0ddf9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.html @@ -0,0 +1,33 @@ +
+ + + + + + + @if (description.errors?.['skyCharacterCounter']) { + + Limit Transaction description to + {{ maxDescriptionCharacterCount }} characters. + + } + + +
diff --git a/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.spec.ts new file mode 100644 index 0000000000..7190dffd88 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.spec.ts @@ -0,0 +1,75 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { SkyCharacterCounterIndicatorHarness } from '@skyux/forms/testing'; +import { SkyStatusIndicatorHarness } from '@skyux/indicators/testing'; + +import { FormsCharacterCountExampleComponent } from './example.component'; + +describe('Character count example', () => { + async function setupTest(): Promise<{ + harness: SkyCharacterCounterIndicatorHarness; + loader: HarnessLoader; + }> { + const fixture = TestBed.createComponent( + FormsCharacterCountExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyCharacterCounterIndicatorHarness.with({ + dataSkyId: 'description-indicator', + }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, loader }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FormsCharacterCountExampleComponent], + }); + }); + + it('should allow a maximum of 50 characters', async () => { + const { harness, loader } = await setupTest(); + + // Validate initial state. + await expectAsync(harness.getCharacterCountLimit()).toBeResolvedTo(50); + await expectAsync(harness.getCharacterCount()).toBeResolvedTo(46); + await expectAsync(harness.isOverLimit()).toBeResolvedTo(false); + + // Update the value to exceed the limit and validate. + const inputEl = + document.querySelector('.description-input'); + + if (inputEl) { + inputEl.value += ' scholarship fund'; + inputEl.dispatchEvent(new Event('input')); + } + + await expectAsync(harness.getCharacterCount()).toBeResolvedTo(63); + await expectAsync(harness.isOverLimit()).toBeResolvedTo(true); + + // Validate that the status indicator error displayed when limit was exceeded. + const statusIndicator = await loader.getHarness( + SkyStatusIndicatorHarness.with({ + dataSkyId: 'description-status-indicator-over-limit', + }), + ); + + await expectAsync(statusIndicator.getDescriptionType()).toBeResolvedTo( + 'error', + ); + await expectAsync(statusIndicator.getIndicatorType()).toBeResolvedTo( + 'danger', + ); + await expectAsync(statusIndicator.getText()).toBeResolvedTo( + 'Limit Transaction description to 50 characters.', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.ts new file mode 100644 index 0000000000..438a97b858 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/character-count/example.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyCharacterCounterModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-forms-character-count-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyCharacterCounterModule, + SkyIdModule, + SkyInputBoxModule, + SkyStatusIndicatorModule, + ], +}) +export class FormsCharacterCountExampleComponent { + protected description: FormControl; + protected formGroup: FormGroup; + protected maxDescriptionCharacterCount = 50; + + readonly #formBuilder = inject(FormBuilder); + + constructor() { + this.description = this.#formBuilder.control( + 'Boys and Girls Club of South Carolina donation', + ); + + this.formGroup = this.#formBuilder.group({ + description: this.description, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.html b/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.html new file mode 100644 index 0000000000..bd6536fea6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.html @@ -0,0 +1,32 @@ +
+
+ + + + + @if (contactMethod.errors?.['emailOnly']) { + + } + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.spec.ts new file mode 100644 index 0000000000..7b05418082 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.spec.ts @@ -0,0 +1,119 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyCheckboxGroupHarness, + SkyCheckboxHarness, +} from '@skyux/forms/testing'; + +import { FormsCheckboxBasicExampleComponent } from './example.component'; + +describe('Basic checkbox group example', () => { + async function setupCheckboxGroupTest(options: { + dataSkyId: string; + }): Promise { + const fixture = TestBed.createComponent(FormsCheckboxBasicExampleComponent); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyCheckboxGroupHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return harness; + } + + async function setupCheckboxTest(options: { + dataSkyId: string; + }): Promise { + const fixture = TestBed.createComponent(FormsCheckboxBasicExampleComponent); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyCheckboxHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return harness; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, FormsCheckboxBasicExampleComponent], + }); + }); + + it('should have the appropriate heading text/level/style, label text, and hint text', async () => { + const harness = await setupCheckboxGroupTest({ + dataSkyId: 'checkbox-group', + }); + + const checkboxButtons = await harness.getCheckboxes(); + + await expectAsync(harness.getHeadingText()).toBeResolvedTo( + 'Contact method', + ); + await expectAsync(harness.getHeadingLevel()).toBeResolvedTo(undefined); + await expectAsync(harness.getHeadingStyle()).toBeResolvedTo(5); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Please select at least one phone-based method.', + ); + + await expectAsync(checkboxButtons[0].getLabelText()).toBeResolvedTo( + 'Email', + ); + await expectAsync(checkboxButtons[0].getHintText()).toBeResolvedTo(''); + + await expectAsync(checkboxButtons[1].getLabelText()).toBeResolvedTo( + 'Phone', + ); + await expectAsync(checkboxButtons[1].getHintText()).toBeResolvedTo(''); + + await expectAsync(checkboxButtons[2].getLabelText()).toBeResolvedTo('Text'); + await expectAsync(checkboxButtons[2].getHintText()).toBeResolvedTo(''); + }); + + it('should display an error message when there is a custom validation error', async () => { + const harness = await setupCheckboxGroupTest({ + dataSkyId: 'checkbox-group', + }); + + const checkboxHarness = (await harness.getCheckboxes())[0]; + + await checkboxHarness.check(); + + await expectAsync(harness.hasError('emailOnly')).toBeResolvedTo(true); + }); + + it('should show a help popover with the expected text', async () => { + const harness = await setupCheckboxGroupTest({ + dataSkyId: 'checkbox-group', + }); + + await harness.clickHelpInline(); + + const helpPopoverContent = await harness.getHelpPopoverContent(); + expect(helpPopoverContent).toBe( + `We use your contact info to keep you informed on current events. We will not sell your information.`, + ); + }); + + it('should check and uncheck checkboxes in display errors if they are required', async () => { + const harness = await setupCheckboxTest({ dataSkyId: 'single-checkbox' }); + + await expectAsync(harness.isStacked()).toBeResolvedTo(true); + + await expectAsync(harness.isChecked()).toBeResolvedTo(false); + await harness.check(); + await expectAsync(harness.isChecked()).toBeResolvedTo(true); + await harness.uncheck(); + await harness.blur(); + await expectAsync(harness.hasRequiredError()).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.ts new file mode 100644 index 0000000000..fe4ad929aa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/basic/example.component.ts @@ -0,0 +1,57 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyCheckboxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-forms-checkbox-basic-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyCheckboxModule], +}) +export class FormsCheckboxBasicExampleComponent { + #formBuilder: FormBuilder = inject(FormBuilder); + + protected formGroup: FormGroup; + protected contactMethod: FormGroup; + + constructor() { + this.contactMethod = this.#formBuilder.group({ + email: new FormControl(false), + phone: new FormControl(false), + text: new FormControl(false), + }); + + this.formGroup = this.#formBuilder.group({ + contactMethod: this.contactMethod, + terms: new FormControl(false), + }); + + this.contactMethod.setValidators( + (control: AbstractControl): ValidationErrors | null => { + const group = control as FormGroup; + const email = group.controls['email']; + const phone = group.controls['phone']; + const text = group.controls['text']; + + if (email.value && !phone.value && !text.value) { + return { emailOnly: true }; + } else { + return null; + } + }, + ); + } + + protected onSubmit(): void { + this.formGroup.markAllAsTouched(); + + console.log(this.formGroup.value); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.html new file mode 100644 index 0000000000..837527bf7b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.html @@ -0,0 +1,33 @@ +
+
+ + + + + @if (contactMethod.errors?.['emailOnly']) { + + } + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.spec.ts new file mode 100644 index 0000000000..9437ca54f5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.spec.ts @@ -0,0 +1,153 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { + SkyCheckboxGroupHarness, + SkyCheckboxHarness, +} from '@skyux/forms/testing'; + +import { FormsCheckboxHelpKeyExampleComponent } from './example.component'; + +describe('Basic checkbox group example', () => { + async function setupCheckboxGroupTest(options: { + dataSkyId: string; + }): Promise<{ + checkboxGroupHarness: SkyCheckboxGroupHarness; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + FormsCheckboxHelpKeyExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const checkboxGroupHarness = await loader.getHarness( + SkyCheckboxGroupHarness.with({ dataSkyId: options.dataSkyId }), + ); + + const helpController = TestBed.inject(SkyHelpTestingController); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { checkboxGroupHarness, helpController }; + } + + async function setupCheckboxTest(options: { dataSkyId: string }): Promise<{ + checkboxHarness: SkyCheckboxHarness; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + FormsCheckboxHelpKeyExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const checkboxHarness = await loader.getHarness( + SkyCheckboxHarness.with({ dataSkyId: options.dataSkyId }), + ); + + const helpController = TestBed.inject(SkyHelpTestingController); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { checkboxHarness, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FormsCheckboxHelpKeyExampleComponent, + SkyHelpTestingModule, + ], + }); + }); + + it('should have the appropriate heading text/level/style, label text, and hint text', async () => { + const { checkboxGroupHarness } = await setupCheckboxGroupTest({ + dataSkyId: 'checkbox-group', + }); + + const checkboxButtons = await checkboxGroupHarness.getCheckboxes(); + + await expectAsync(checkboxGroupHarness.getHeadingText()).toBeResolvedTo( + 'Contact method', + ); + await expectAsync(checkboxGroupHarness.getHeadingLevel()).toBeResolvedTo( + undefined, + ); + await expectAsync(checkboxGroupHarness.getHeadingStyle()).toBeResolvedTo(5); + await expectAsync(checkboxGroupHarness.getHintText()).toBeResolvedTo( + 'Please select at least one phone-based method.', + ); + + await expectAsync(checkboxButtons[0].getLabelText()).toBeResolvedTo( + 'Email', + ); + await expectAsync(checkboxButtons[0].getHintText()).toBeResolvedTo(''); + + await expectAsync(checkboxButtons[1].getLabelText()).toBeResolvedTo( + 'Phone', + ); + await expectAsync(checkboxButtons[1].getHintText()).toBeResolvedTo(''); + + await expectAsync(checkboxButtons[2].getLabelText()).toBeResolvedTo('Text'); + await expectAsync(checkboxButtons[2].getHintText()).toBeResolvedTo(''); + }); + + it('should display an error message when there is a custom validation error', async () => { + const { checkboxGroupHarness } = await setupCheckboxGroupTest({ + dataSkyId: 'checkbox-group', + }); + + const checkboxHarness = (await checkboxGroupHarness.getCheckboxes())[0]; + + await checkboxHarness.check(); + + await expectAsync( + checkboxGroupHarness.hasError('emailOnly'), + ).toBeResolvedTo(true); + }); + + it('should have the correct help key for checkbox groups', async () => { + const { checkboxGroupHarness, helpController } = + await setupCheckboxGroupTest({ + dataSkyId: 'checkbox-group', + }); + + await checkboxGroupHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('contact-help'); + }); + + it('should check and uncheck checkboxes in display errors if they are required', async () => { + const { checkboxHarness } = await setupCheckboxTest({ + dataSkyId: 'single-checkbox', + }); + + await expectAsync(checkboxHarness.isStacked()).toBeResolvedTo(true); + + await expectAsync(checkboxHarness.isChecked()).toBeResolvedTo(false); + await checkboxHarness.check(); + await expectAsync(checkboxHarness.isChecked()).toBeResolvedTo(true); + await checkboxHarness.uncheck(); + await checkboxHarness.blur(); + await expectAsync(checkboxHarness.hasRequiredError()).toBeResolvedTo(true); + }); + + it('should have the correct help key for checkboxes', async () => { + const { checkboxHarness, helpController } = await setupCheckboxTest({ + dataSkyId: 'single-checkbox', + }); + + await checkboxHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('terms-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.ts new file mode 100644 index 0000000000..0a15a9deb6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/help-key/example.component.ts @@ -0,0 +1,57 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyCheckboxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-forms-checkbox-help-key-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyCheckboxModule], +}) +export class FormsCheckboxHelpKeyExampleComponent { + #formBuilder: FormBuilder = inject(FormBuilder); + + protected formGroup: FormGroup; + protected contactMethod: FormGroup; + + constructor() { + this.contactMethod = this.#formBuilder.group({ + email: new FormControl(false), + phone: new FormControl(false), + text: new FormControl(false), + }); + + this.formGroup = this.#formBuilder.group({ + contactMethod: this.contactMethod, + terms: new FormControl(false), + }); + + this.contactMethod.setValidators( + (control: AbstractControl): ValidationErrors | null => { + const group = control as FormGroup; + const email = group.controls['email']; + const phone = group.controls['phone']; + const text = group.controls['text']; + + if (email.value && !phone.value && !text.value) { + return { emailOnly: true }; + } else { + return null; + } + }, + ); + } + + protected onSubmit(): void { + this.formGroup.markAllAsTouched(); + + console.log(this.formGroup.value); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.html b/libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.html new file mode 100644 index 0000000000..d153bb9d97 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.html @@ -0,0 +1,30 @@ +
+
+ + + + + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.ts new file mode 100644 index 0000000000..c13c53a54a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/checkbox/icon-group/example.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyCheckboxModule } from '@skyux/forms'; + +@Component({ + selector: 'app-forms-checkbox-icon-group-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyCheckboxModule], +}) +export class FormsCheckboxIconGroupExampleComponent { + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + bold: new FormControl(false), + italic: new FormControl(false), + underline: new FormControl(false), + }); + } + + public onSubmit(): void { + console.log(this.formGroup.value); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.html b/libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.html new file mode 100644 index 0000000000..66cd54f7a3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.html @@ -0,0 +1,48 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.ts new file mode 100644 index 0000000000..251bc9c6ab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/field-group/basic/example.component.ts @@ -0,0 +1,91 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyFieldGroupModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyFluidGridModule } from '@skyux/layout'; + +@Component({ + selector: 'app-forms-field-group-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyFieldGroupModule, + SkyFluidGridModule, + SkyInputBoxModule, + ], +}) +export class FormsFieldGroupBasicExampleComponent { + #formBuilder: FormBuilder = inject(FormBuilder); + + protected formGroup: FormGroup; + protected helpPopoverContent = + 'We use your address to validate your application with regulatory agencies and to send correspondence related to your application.'; + + protected states = [ + 'AK', + 'AZ', + 'AL', + 'AR', + 'CA', + 'CO', + 'CT', + 'DE', + 'DC', + 'FL', + 'GA', + 'HI', + 'ID', + 'IL', + 'IN', + 'IA', + 'KS', + 'KY', + 'LA', + 'ME', + 'MD', + 'MA', + 'MI', + 'MN', + 'MS', + 'MO', + 'MT', + 'NE', + 'NV', + 'NH', + 'NJ', + 'NM', + 'NY', + 'NC', + 'ND', + 'OH', + 'OK', + 'OR', + 'PA', + 'RI', + 'SC', + 'SD', + 'TN', + 'TX', + 'UT', + 'VT', + 'VA', + 'WA', + 'WV', + 'WI', + 'WY', + ]; + + constructor() { + this.formGroup = this.#formBuilder.group({ + streetAddress: this.#formBuilder.control(undefined), + city: this.#formBuilder.control(undefined), + state: this.#formBuilder.control(undefined), + zipCode: this.#formBuilder.control(undefined), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.html new file mode 100644 index 0000000000..60f984eee5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.html @@ -0,0 +1,48 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.spec.ts new file mode 100644 index 0000000000..1454dd4ca5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.spec.ts @@ -0,0 +1,40 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyFieldGroupHarness } from '@skyux/forms/testing'; + +import { FormsFieldGroupHelpKeyExampleComponent } from './example.component'; + +describe('Field group', () => { + async function setupTest(): Promise<{ + fieldGroupHarness: SkyFieldGroupHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + FormsFieldGroupHelpKeyExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + const fieldGroupHarness = await loader.getHarness(SkyFieldGroupHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { fieldGroupHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FormsFieldGroupHelpKeyExampleComponent, SkyHelpTestingModule], + }); + }); + + it('should have the correct help key', async () => { + const { fieldGroupHarness, helpController } = await setupTest(); + + await fieldGroupHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('address-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.ts new file mode 100644 index 0000000000..314922f0f6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/field-group/help-key/example.component.ts @@ -0,0 +1,88 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyFieldGroupModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyFluidGridModule } from '@skyux/layout'; + +@Component({ + selector: 'app-forms-field-group-help-key-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyFieldGroupModule, + SkyFluidGridModule, + SkyInputBoxModule, + ], +}) +export class FormsFieldGroupHelpKeyExampleComponent { + #formBuilder: FormBuilder = inject(FormBuilder); + + protected formGroup: FormGroup; + protected states = [ + 'AK', + 'AZ', + 'AL', + 'AR', + 'CA', + 'CO', + 'CT', + 'DE', + 'DC', + 'FL', + 'GA', + 'HI', + 'ID', + 'IL', + 'IN', + 'IA', + 'KS', + 'KY', + 'LA', + 'ME', + 'MD', + 'MA', + 'MI', + 'MN', + 'MS', + 'MO', + 'MT', + 'NE', + 'NV', + 'NH', + 'NJ', + 'NM', + 'NY', + 'NC', + 'ND', + 'OH', + 'OK', + 'OR', + 'PA', + 'RI', + 'SC', + 'SD', + 'TN', + 'TX', + 'UT', + 'VT', + 'VA', + 'WA', + 'WV', + 'WI', + 'WY', + ]; + + constructor() { + this.formGroup = this.#formBuilder.group({ + streetAddress: this.#formBuilder.control(undefined), + city: this.#formBuilder.control(undefined), + state: this.#formBuilder.control(undefined), + zipCode: this.#formBuilder.control(undefined), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.html b/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.html new file mode 100644 index 0000000000..441110f7fa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.html @@ -0,0 +1,31 @@ +
+ + @if (attachment.errors?.['invalidStartingLetter']) { + + } + +
+ + + Make sure the birth certificate includes: +
    +
  • Full name of child
  • +
  • Birth date
  • +
  • Place of birth
  • +
  • Name and signature of the physician or midwife
  • +
  • Registration or certificate number
  • +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.spec.ts new file mode 100644 index 0000000000..2a57e40dc9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.spec.ts @@ -0,0 +1,75 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyFileAttachmentHarness, + provideSkyFileAttachmentTesting, +} from '@skyux/forms/testing'; + +import { FormsFileAttachmentBasicExampleComponent } from './example.component'; + +describe('Basic file attachment example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyFileAttachmentHarness; + fixture: ComponentFixture; + }> { + TestBed.configureTestingModule({ + providers: [provideSkyFileAttachmentTesting()], + }); + const fixture = TestBed.createComponent( + FormsFileAttachmentBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyFileAttachmentHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FormsFileAttachmentBasicExampleComponent, NoopAnimationsModule], + }); + }); + + it('should set initial values', async () => { + const { harness } = await setupTest({ + dataSkyId: 'birth-certificate', + }); + + await expectAsync(harness.getLabelText()).toBeResolvedTo( + 'Birth certificate', + ); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Attach a .pdf, .gif, .png, or .jpeg file.', + ); + await expectAsync(harness.getAcceptedTypes()).toBeResolvedTo( + 'application/pdf,image/jpeg,image/png,image/gif', + ); + await expectAsync(harness.isRequired()).toBeResolvedTo(true); + + await harness.clickHelpInline(); + + await expectAsync(harness.getHelpPopoverTitle()).toBeResolvedTo( + 'Requirements', + ); + }); + + it('should throw an error if file begins with the letter a', async () => { + const { harness } = await setupTest({ + dataSkyId: 'birth-certificate', + }); + + const file = new File([], 'art.png', { type: 'image/png' }); + await harness.attachFile(file); + + await expectAsync( + harness.hasCustomError('invalidStartingLetter'), + ).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.ts new file mode 100644 index 0000000000..2097ab4f93 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-attachment/basic/example.component.ts @@ -0,0 +1,64 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { + SkyFileAttachmentClick, + SkyFileAttachmentModule, + SkyFileItem, +} from '@skyux/forms'; + +/** + * Demonstrates how to create a custom validator function for your form control. + */ +function customValidator( + control: AbstractControl, +): ValidationErrors | null { + const fileItem = control.value; + + return fileItem?.file?.name.startsWith('a') + ? { invalidStartingLetter: true } + : null; +} + +@Component({ + selector: 'app-forms-file-attachment-basic-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyFileAttachmentModule], +}) +export class FormsFileAttachmentBasicExampleComponent { + protected attachment: FormControl; + + protected formGroup: FormGroup<{ + attachment: FormControl; + }>; + + protected maxFileSize = 4000000; + + constructor() { + this.attachment = new FormControl(undefined, { + validators: [Validators.required, customValidator], + }); + + this.formGroup = inject(FormBuilder).group({ + attachment: this.attachment, + }); + } + + protected onFileClick($event: SkyFileAttachmentClick): void { + // Ensure we are only attempting to navigate to locally updated data for download. + if ($event.file.url.startsWith('data:')) { + const link = document.createElement('a'); + link.download = $event.file.file.name; + link.href = $event.file.url; + link.click(); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.html new file mode 100644 index 0000000000..2c5054b8e9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.html @@ -0,0 +1,18 @@ +
+ + @if (attachment.errors?.['invalidStartingLetter']) { + + } + +
diff --git a/libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.ts new file mode 100644 index 0000000000..caa2154877 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-attachment/help-key/example.component.ts @@ -0,0 +1,64 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { + SkyFileAttachmentClick, + SkyFileAttachmentModule, + SkyFileItem, +} from '@skyux/forms'; + +/** + * Demonstrates how to create a custom validator function for your form control. + */ +function customValidator( + control: AbstractControl, +): ValidationErrors | null { + const fileItem = control.value; + + return fileItem?.file?.name.startsWith('a') + ? { invalidStartingLetter: true } + : null; +} + +@Component({ + selector: 'app-forms-file-attachment-help-key-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyFileAttachmentModule], +}) +export class FormsFileAttachmentHelpKeyExampleComponent { + protected attachment: FormControl; + + protected formGroup: FormGroup<{ + attachment: FormControl; + }>; + + protected maxFileSize = 4000000; + + constructor() { + this.attachment = new FormControl(undefined, { + validators: [Validators.required, customValidator], + }); + + this.formGroup = inject(FormBuilder).group({ + attachment: this.attachment, + }); + } + + protected onFileClick($event: SkyFileAttachmentClick): void { + // Ensure we are only attempting to navigate to locally updated data for download. + if ($event.file.url.startsWith('data:')) { + const link = document.createElement('a'); + link.download = $event.file.file.name; + link.href = $event.file.url; + link.click(); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.html b/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.html new file mode 100644 index 0000000000..b25e108925 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.html @@ -0,0 +1,26 @@ +
+ + @if (fileDrop.errors?.['maxNumberOfFilesReached']) { + + } + +
+ +@for (file of fileDrop.value; track file) { + +} diff --git a/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.spec.ts new file mode 100644 index 0000000000..b913ae764d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.spec.ts @@ -0,0 +1,113 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyFileDropHarness, + SkyFileItemHarness, + provideSkyFileAttachmentTesting, +} from '@skyux/forms/testing'; + +import { FormsFileDropBasicExampleComponent } from './example.component'; + +describe('Basic file drop example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyFileDropHarness; + formControl: FormControl; + loader: HarnessLoader; + }> { + TestBed.configureTestingModule({ + providers: [provideSkyFileAttachmentTesting()], + }); + const fixture = TestBed.createComponent(FormsFileDropBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyFileDropHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + const formControl = fixture.componentInstance.fileDrop; + + return { harness, formControl, loader }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FormsFileDropBasicExampleComponent, NoopAnimationsModule], + }); + }); + + it('should set initial values', async () => { + const { harness } = await setupTest({ + dataSkyId: 'logo-upload', + }); + + await expectAsync(harness.getLabelText()).toBeResolvedTo('Logo image'); + await expectAsync(harness.getAcceptedTypes()).toBeResolvedTo( + 'image/png,image/jpeg', + ); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Upload up to 3 files under 50MB.', + ); + await expectAsync(harness.isStacked()).toBeResolvedTo(true); + + await expectAsync(harness.getLinkUploadHintText()).toBeResolvedTo( + 'Start with http:// or https://', + ); + }); + + it('should not upload invalid files starting with `a`', async () => { + const { harness, formControl } = await setupTest({ + dataSkyId: 'logo-upload', + }); + + const filesToUpload: File[] = [ + new File([], 'aWrongFile', { type: 'image/png' }), + new File([], 'validFile', { type: 'image/png' }), + ]; + + await harness.loadFiles(filesToUpload); + + expect(formControl.value?.length).toBe(1); + await expectAsync(harness.hasValidateFnError()).toBeResolvedTo(true); + }); + + it('should not allow more than 3 files to be uploaded', async () => { + const { harness, formControl, loader } = await setupTest({ + dataSkyId: 'logo-upload', + }); + + await harness.loadFiles([ + new File([], 'validFile1', { type: 'image/png' }), + new File([], 'validFile2', { type: 'image/png' }), + new File([], 'validFile3', { type: 'image/png' }), + ]); + + expect(formControl.value?.length).toBe(3); + await expectAsync( + harness.hasCustomError('maxNumberOfFilesReached'), + ).toBeResolvedTo(false); + expect(formControl.valid).toBe(true); + + await harness.enterLinkUploadText('foo.bar'); + await harness.clickLinkUploadDoneButton(); + + expect(formControl.value?.length).toBe(4); + await expectAsync( + harness.hasCustomError('maxNumberOfFilesReached'), + ).toBeResolvedTo(true); + expect(formControl.valid).toBe(false); + + const validFileItemHarness = await loader.getHarness( + SkyFileItemHarness.with({ fileName: 'validFile2' }), + ); + await validFileItemHarness.clickDeleteButton(); + + expect(formControl.value?.length).toBe(3); + expect(formControl.valid).toBe(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.ts new file mode 100644 index 0000000000..860148b670 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-drop/basic/example.component.ts @@ -0,0 +1,79 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyFileDropModule, SkyFileItem, SkyFileLink } from '@skyux/forms'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; + +/** + * Demonstrates how to create a custom validator function for your form control. + */ +function customValidator( + control: AbstractControl<(SkyFileItem | SkyFileLink)[] | null | undefined>, +): ValidationErrors | null { + if (control.value !== undefined && control.value !== null) { + if (control.value.length > 3) { + return { maxNumberOfFilesReached: true }; + } + } + return null; +} + +@Component({ + selector: 'app-forms-file-drop-basic-example', + templateUrl: './example.component.html', + imports: [ + SkyFileDropModule, + SkyStatusIndicatorModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + ], +}) +export class FormsFileDropBasicExampleComponent { + protected acceptedTypes = 'image/png,image/jpeg'; + protected hintText = 'Upload up to 3 files under 50MB.'; + protected inlineHelpContent = + 'Your logo appears in places such as authentication pages, student and parent portals, and extracurricular home pages.'; + protected labelText = 'Logo image'; + protected maxFileSize = 5242880; + protected stacked = 'true'; + + public fileDrop = new FormControl< + (SkyFileItem | SkyFileLink)[] | null | undefined + >(undefined, [Validators.required, customValidator]); + public formGroup: FormGroup = inject(FormBuilder).group({ + fileDrop: this.fileDrop, + }); + + protected deleteFile(file: SkyFileItem | SkyFileLink): void { + const index = this.fileDrop.value?.indexOf(file); + + if (index !== undefined && index !== -1) { + this.fileDrop.value?.splice(index, 1); + /* + If you are adding custom validation through the form control, + be sure to include this line after deleting a file from the form. + */ + this.fileDrop.updateValueAndValidity(); + } + // To ensure that empty arrays throw required errors, include this check. + if (this.fileDrop.value?.length === 0) { + this.fileDrop.setValue(null); + } + } + + protected validateFile(file: SkyFileItem): string | undefined { + return file.file.name.startsWith('a') + ? 'Upload a file that does not begin with the letter "a"' + : undefined; + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.html new file mode 100644 index 0000000000..0d4ed243e1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.html @@ -0,0 +1,19 @@ + +@for (file of allItems; track file) { +
+ +
+} diff --git a/libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.ts new file mode 100644 index 0000000000..4ab84f29e4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/file-drop/help-key/example.component.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; +import { + SkyFileDropChange, + SkyFileDropModule, + SkyFileItem, + SkyFileLink, +} from '@skyux/forms'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-forms-file-drop-help-key-example', + templateUrl: './example.component.html', + imports: [SkyFileDropModule, SkyStatusIndicatorModule], +}) +export class FormsFileDropHelpKeyExampleComponent { + protected acceptedTypes = 'image/png,image/jpeg'; + protected allItems: (SkyFileItem | SkyFileLink)[] = []; + protected hintText = '5 MB maximum'; + protected labelText = 'Logo image'; + protected maxFileSize = 5242880; + protected rejectedFiles: SkyFileItem[] = []; + protected required = true; + protected stacked = 'true'; + + #filesToUpload: SkyFileItem[] = []; + #linksToUpload: SkyFileLink[] = []; + + protected deleteFile(file: SkyFileItem | SkyFileLink): void { + this.#removeFromArray(this.allItems, file); + this.#removeFromArray(this.#filesToUpload, file); + this.#removeFromArray(this.#linksToUpload, file); + } + + protected onFilesChanged(change: SkyFileDropChange): void { + this.#filesToUpload = this.#filesToUpload.concat(change.files); + this.rejectedFiles = change.rejectedFiles; + this.allItems = this.allItems.concat(change.files); + } + + protected onLinkChanged(change: SkyFileLink): void { + this.#linksToUpload = this.#linksToUpload.concat(change); + this.allItems = this.allItems.concat(change); + } + + protected validateFile(file: SkyFileItem): string | undefined { + return file.file.name.startsWith('a') + ? 'Upload a file that does not begin with the letter "a"' + : undefined; + } + + #removeFromArray( + items: (SkyFileItem | SkyFileLink)[], + obj: SkyFileItem | SkyFileLink, + ): void { + if (items) { + const index = items.indexOf(obj); + + if (index !== -1) { + items.splice(index, 1); + } + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.html b/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.html new file mode 100644 index 0000000000..08d5a1d9c6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.html @@ -0,0 +1,99 @@ +
+
+ + + +

New member form

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if (favoriteColor.errors?.['invalid']) { + + } + + + +
+
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.spec.ts new file mode 100644 index 0000000000..e2f6547e5f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.spec.ts @@ -0,0 +1,151 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyAppTestUtility } from '@skyux-sdk/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; + +import { FormsInputBoxBasicExampleComponent } from './example.component'; + +describe('Basic input box example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + inputBoxHarness: SkyInputBoxHarness; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent(FormsInputBoxBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const helpController = TestBed.inject(SkyHelpTestingController); + const inputBoxHarness = await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { inputBoxHarness, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FormsInputBoxBasicExampleComponent, + SkyHelpTestingModule, + ], + }); + }); + + describe('first name field', () => { + it('should have the expected label text and stacked values', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'input-box-first-name', + }); + + await expectAsync(inputBoxHarness.getLabelText()).toBeResolvedTo( + 'First name', + ); + await expectAsync(inputBoxHarness.getStacked()).toBeResolvedTo(true); + }); + + it('should have the correct help key', async () => { + const { inputBoxHarness, helpController } = await setupTest({ + dataSkyId: 'input-box-first-name', + }); + + await inputBoxHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('first-name-help'); + }); + }); + + describe('last name field', () => { + it('should have last name required', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'input-box-last-name', + }); + + const inputEl = document.querySelector( + 'input.last-name-input-box', + ); + + if (inputEl) { + inputEl.value = ''; + SkyAppTestUtility.fireDomEvent(inputEl, 'input'); + SkyAppTestUtility.fireDomEvent(inputEl, 'blur'); + } + + await expectAsync(inputBoxHarness.hasRequiredError()).toBeResolvedTo( + true, + ); + }); + }); + + describe('bio field', () => { + it('should have a character limit of 250', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'input-box-bio', + }); + + const characterCounter = await inputBoxHarness.getCharacterCounter(); + + await expectAsync(characterCounter.getCharacterCount()).toBeResolvedTo(0); + await expectAsync( + characterCounter.getCharacterCountLimit(), + ).toBeResolvedTo(250); + }); + + it('should show hint text', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'input-box-bio', + }); + + const hintText = await inputBoxHarness.getHintText(); + + expect(hintText).toBe( + `A brief description of the member's background, such as hometown, school, hobbies, etc.`, + ); + }); + }); + + describe('email field', () => { + it('should show a help popover with the expected text', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'input-box-email', + }); + + await inputBoxHarness.clickHelpInline(); + + const helpPopoverTitle = await inputBoxHarness.getHelpPopoverTitle(); + expect(helpPopoverTitle).toBe('Privacy notice'); + + const helpPopoverContent = await inputBoxHarness.getHelpPopoverContent(); + expect(helpPopoverContent).toBe( + `We do not share this information with any third parties.`, + ); + }); + }); + + describe('favorite color field', () => { + it('should not allow invalid color to be selected', async () => { + const { inputBoxHarness } = await setupTest({ + dataSkyId: 'input-box-favorite-color', + }); + + const selectEl = document.querySelector( + '.input-box-favorite-color-select', + ); + + if (selectEl) { + selectEl.value = 'invalid'; + selectEl.dispatchEvent(new Event('change')); + } + + await expectAsync( + inputBoxHarness.hasCustomFormError('invalid'), + ).toBeResolvedTo(true); + }); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.ts new file mode 100644 index 0000000000..90808b201a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/input-box/basic/example.component.ts @@ -0,0 +1,61 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyDatepickerModule } from '@skyux/datetime'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; +import { SkyFluidGridModule } from '@skyux/layout'; +import { SkyValidators } from '@skyux/validation'; + +@Component({ + selector: 'app-forms-input-box-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyDatepickerModule, + SkyFluidGridModule, + SkyInputBoxModule, + SkyStatusIndicatorModule, + ], +}) +export class FormsInputBoxBasicExampleComponent { + protected favoriteColor: FormControl; + + protected formGroup: FormGroup<{ + firstName: FormControl; + lastName: FormControl; + bio: FormControl; + email: FormControl; + dob: FormControl; + favoriteColor: FormControl; + }>; + + constructor() { + this.favoriteColor = new FormControl('none', [ + (control): ValidationErrors | null => { + if (control.value === 'invalid') { + return { invalid: true }; + } + + return null; + }, + ]); + + this.formGroup = inject(FormBuilder).group({ + firstName: new FormControl(''), + lastName: new FormControl('', Validators.required), + bio: new FormControl(''), + email: new FormControl('', [Validators.required, SkyValidators.email]), + dob: new FormControl('', Validators.required), + favoriteColor: this.favoriteColor, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.html new file mode 100644 index 0000000000..09957fc2b7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.html @@ -0,0 +1,28 @@ +
+
+ + @for (option of paymentOptions; track option) { + + } + @if (paymentMethod.errors?.['processingIssue']) { + + } + +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.spec.ts new file mode 100644 index 0000000000..8c796b360a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.spec.ts @@ -0,0 +1,107 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyRadioGroupHarness } from '@skyux/forms/testing'; + +import { FormsRadioHelpKeyExampleComponent } from './example.component'; + +describe('Basic radio group example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + radioGroupHarness: SkyRadioGroupHarness; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent(FormsRadioHelpKeyExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const helpController = TestBed.inject(SkyHelpTestingController); + + const radioGroupHarness = await loader.getHarness( + SkyRadioGroupHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { radioGroupHarness, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + FormsRadioHelpKeyExampleComponent, + SkyHelpTestingModule, + ], + }); + }); + + it('should have the appropriate heading text/level/style, label text, and hint text', async () => { + const { radioGroupHarness } = await setupTest({ dataSkyId: 'radio-group' }); + + const radioButtons = await radioGroupHarness.getRadioButtons(); + + await expectAsync(radioGroupHarness.getHeadingText()).toBeResolvedTo( + 'Payment method', + ); + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(4); + await expectAsync(radioGroupHarness.getHeadingStyle()).toBeResolvedTo(4); + await expectAsync(radioGroupHarness.getHintText()).toBeResolvedTo( + 'Card methods require proof of identification.', + ); + + await expectAsync(radioButtons[0].getLabelText()).toBeResolvedTo('Cash'); + await expectAsync(radioButtons[0].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[1].getLabelText()).toBeResolvedTo('Check'); + await expectAsync(radioButtons[1].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[2].getLabelText()).toBeResolvedTo( + 'Apple pay', + ); + await expectAsync(radioButtons[2].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[3].getLabelText()).toBeResolvedTo('Credit'); + await expectAsync(radioButtons[3].getHintText()).toBeResolvedTo( + 'A 2% late fee is applied to payments made after the due date.', + ); + + await expectAsync(radioButtons[4].getLabelText()).toBeResolvedTo('Debit'); + await expectAsync(radioButtons[4].getHintText()).toBeResolvedTo(''); + }); + + it('should display an error message when there is a custom validation error', async () => { + const { radioGroupHarness } = await setupTest({ dataSkyId: 'radio-group' }); + + const radioHarness = (await radioGroupHarness.getRadioButtons())[1]; + + await radioHarness.check(); + + await expectAsync( + radioGroupHarness.hasError('processingIssue'), + ).toBeResolvedTo(true); + }); + + it('should have the correct help key for radio group', async () => { + const { radioGroupHarness, helpController } = await setupTest({ + dataSkyId: 'radio-group', + }); + + await radioGroupHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('payment-help'); + }); + + it('should have the correct help key for radio', async () => { + const { radioGroupHarness, helpController } = await setupTest({ + dataSkyId: 'radio-group', + }); + const radioHarness = (await radioGroupHarness.getRadioButtons())[0]; + + await radioHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('cash-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.ts new file mode 100644 index 0000000000..447ce95f8b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/help-key/example.component.ts @@ -0,0 +1,71 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyRadioModule } from '@skyux/forms'; + +interface DemoForm { + paymentMethod: FormControl; +} + +interface Item { + name: string; + value: string; + disabled?: boolean; + hintText?: string; + helpKey?: string; +} + +function validatePaymentMethod( + control: AbstractControl, +): ValidationErrors | null { + return control.value === 'check' ? { processingIssue: true } : null; +} + +@Component({ + selector: 'app-forms-radio-help-key-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyRadioModule], +}) +export class FormsRadioHelpKeyExampleComponent { + protected formGroup: FormGroup; + protected hintText = 'Card methods require proof of identification.'; + protected paymentMethod: FormControl; + + protected paymentOptions: Item[] = [ + { + name: 'Cash', + value: 'cash', + helpKey: 'cash-help', + }, + { name: 'Check', value: 'check' }, + { name: 'Apple pay', value: 'apple', disabled: true }, + { + name: 'Credit', + value: 'credit', + hintText: 'A 2% late fee is applied to payments made after the due date.', + }, + { name: 'Debit', value: 'debit' }, + ]; + + readonly #formBuilder = inject(FormBuilder); + + constructor() { + this.paymentMethod = this.#formBuilder.control( + this.paymentOptions[0].name, + { + validators: [validatePaymentMethod], + }, + ); + + this.formGroup = this.#formBuilder.group({ + paymentMethod: this.paymentMethod, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.html b/libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.html new file mode 100644 index 0000000000..a1db9accd9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.html @@ -0,0 +1,19 @@ +
+
+ + @for (view of views; track view) { + + } + +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.ts new file mode 100644 index 0000000000..412461f69b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/icon/example.component.ts @@ -0,0 +1,35 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyRadioModule } from '@skyux/forms'; + +interface Item { + icon: string; + label: string; + name: string; +} + +@Component({ + selector: 'app-forms-radio-icon-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyRadioModule], +}) +export class FormsRadioIconExampleComponent { + protected formGroup: FormGroup; + + protected views: Item[] = [ + { icon: 'table', label: 'Table', name: 'table' }, + { icon: 'list', label: 'List', name: 'list' }, + { icon: 'map-marker', label: 'Map', name: 'map' }, + ]; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + myView: this.views[0].name, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.html b/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.html new file mode 100644 index 0000000000..8e78bd76eb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.html @@ -0,0 +1,29 @@ +
+
+ + @for (option of paymentOptions; track option) { + + } + @if (paymentMethod.errors?.['processingIssue']) { + + } + +
+
diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.spec.ts new file mode 100644 index 0000000000..84d421c4e7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.spec.ts @@ -0,0 +1,91 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyRadioGroupHarness } from '@skyux/forms/testing'; + +import { FormsRadioStandardExampleComponent } from './example.component'; + +describe('Basic radio group example', () => { + async function setupTest(options: { + dataSkyId: string; + }): Promise { + const fixture = TestBed.createComponent(FormsRadioStandardExampleComponent); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyRadioGroupHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return harness; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, FormsRadioStandardExampleComponent], + }); + }); + + it('should have the appropriate heading text/level/style, label text, and hint text', async () => { + const harness = await setupTest({ dataSkyId: 'radio-group' }); + + const radioButtons = await harness.getRadioButtons(); + + await expectAsync(harness.getHeadingText()).toBeResolvedTo( + 'Payment method', + ); + await expectAsync(harness.getHeadingLevel()).toBeResolvedTo(4); + await expectAsync(harness.getHeadingStyle()).toBeResolvedTo(4); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Card methods require proof of identification.', + ); + + await expectAsync(radioButtons[0].getLabelText()).toBeResolvedTo('Cash'); + await expectAsync(radioButtons[0].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[1].getLabelText()).toBeResolvedTo('Check'); + await expectAsync(radioButtons[1].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[2].getLabelText()).toBeResolvedTo( + 'Apple pay', + ); + await expectAsync(radioButtons[2].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[3].getLabelText()).toBeResolvedTo('Credit'); + await expectAsync(radioButtons[3].getHintText()).toBeResolvedTo( + 'A 2% late fee is applied to payments made after the due date.', + ); + + await expectAsync(radioButtons[4].getLabelText()).toBeResolvedTo('Debit'); + await expectAsync(radioButtons[4].getHintText()).toBeResolvedTo(''); + }); + + it('should display an error message when there is a custom validation error', async () => { + const harness = await setupTest({ dataSkyId: 'radio-group' }); + + const radioHarness = (await harness.getRadioButtons())[1]; + + await radioHarness.check(); + + await expectAsync(harness.hasError('processingIssue')).toBeResolvedTo(true); + }); + + it('should show a help popover with the expected text', async () => { + const harness = await setupTest({ + dataSkyId: 'radio-group', + }); + + await harness.clickHelpInline(); + + const helpPopoverTitle = await harness.getHelpPopoverTitle(); + expect(helpPopoverTitle).toBe('Are there fees?'); + + const helpPopoverContent = await harness.getHelpPopoverContent(); + expect(helpPopoverContent).toBe( + `We don't charge fees for any payment method. The only exception is when credit card payments are late, which incurs a 2% fee.`, + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.ts new file mode 100644 index 0000000000..03c1cd72d3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/radio/standard/example.component.ts @@ -0,0 +1,75 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyRadioModule } from '@skyux/forms'; + +interface DemoForm { + paymentMethod: FormControl; +} + +interface Item { + name: string; + value: string; + disabled?: boolean; + hintText?: string; + helpContent?: string; +} + +function validatePaymentMethod( + control: AbstractControl, +): ValidationErrors | null { + return control.value === 'check' ? { processingIssue: true } : null; +} + +@Component({ + selector: 'app-forms-radio-standard-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyRadioModule], +}) +export class FormsRadioStandardExampleComponent { + protected formGroup: FormGroup; + protected helpPopoverContent = + "We don't charge fees for any payment method. The only exception is when credit card payments are late, which incurs a 2% fee."; + protected helpPopoverTitle = 'Are there fees?'; + protected hintText = 'Card methods require proof of identification.'; + protected paymentMethod: FormControl; + + protected paymentOptions: Item[] = [ + { + name: 'Cash', + value: 'cash', + helpContent: + 'We accept cash at any of our locations and affiliated partners.', + }, + { name: 'Check', value: 'check' }, + { name: 'Apple pay', value: 'apple', disabled: true }, + { + name: 'Credit', + value: 'credit', + hintText: 'A 2% late fee is applied to payments made after the due date.', + }, + { name: 'Debit', value: 'debit' }, + ]; + + readonly #formBuilder = inject(FormBuilder); + + constructor() { + this.paymentMethod = this.#formBuilder.control( + this.paymentOptions[0].name, + { + validators: [validatePaymentMethod], + }, + ); + + this.formGroup = this.#formBuilder.group({ + paymentMethod: this.paymentMethod, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.html b/libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.html new file mode 100644 index 0000000000..d8ca66e084 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.html @@ -0,0 +1,20 @@ +
+ + @for (control of checkboxControls; track control; let i = $index) { + + + + {{ selectionBoxes[i].name }} + + + {{ selectionBoxes[i].description }} + + + + } + +
diff --git a/libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.ts new file mode 100644 index 0000000000..fce3dca54c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/selection-box/checkbox/example.component.ts @@ -0,0 +1,74 @@ +import { Component, inject } from '@angular/core'; +import { + FormArray, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyCheckboxModule, SkySelectionBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; + +@Component({ + selector: 'app-forms-selection-box-checkbox-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyCheckboxModule, + SkyIconModule, + SkyIdModule, + SkySelectionBoxModule, + ], +}) +export class FormsSelectionBoxCheckboxExampleComponent { + protected checkboxControls: FormControl[] | undefined; + + protected selectionBoxes: { + description: string; + icon: string; + name: string; + selected?: boolean; + }[] = [ + { + name: 'Save time and effort', + icon: 'clock', + description: + 'Automate mundane tasks and spend more time on the things that matter.', + }, + { + name: 'Boost engagement', + icon: 'user', + description: 'Encourage supporters to interact with your organization.', + }, + { + name: 'Build relationships', + icon: 'users', + description: + 'Connect to supporters on a personal level and maintain accurate data.', + }, + ]; + + protected formGroup: FormGroup; + + readonly #formBuilder = inject(FormBuilder); + + constructor() { + const checkboxArray = this.#buildCheckboxes(); + this.checkboxControls = checkboxArray.controls as FormControl[]; + + this.formGroup = this.#formBuilder.group({ + checkboxes: checkboxArray, + }); + } + + #buildCheckboxes(): FormArray { + const checkboxArray = this.selectionBoxes.map((checkbox) => + this.#formBuilder.control(checkbox.selected), + ); + + return this.#formBuilder.array(checkboxArray); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.html b/libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.html new file mode 100644 index 0000000000..c77dabfd0f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.html @@ -0,0 +1,22 @@ +
+ + + @for (item of items; track item) { + + + + {{ item['name'] }} + + + {{ item['description'] }} + + + + } + + +
diff --git a/libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.ts new file mode 100644 index 0000000000..718e2aaa35 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/selection-box/radio/example.component.ts @@ -0,0 +1,55 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyRadioModule, SkySelectionBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; + +@Component({ + selector: 'app-forms-selection-box-radio-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyIconModule, + SkyIdModule, + SkyRadioModule, + SkySelectionBoxModule, + ], +}) +export class FormsSelectionBoxRadioExampleComponent { + protected items: Record[] = [ + { + name: 'Save time and effort', + icon: 'clock', + description: + 'Automate mundane tasks and spend more time on the things that matter.', + value: 'clock', + }, + { + name: 'Boost engagement', + icon: 'user', + description: 'Encourage supporters to interact with your organization.', + value: 'engagement', + }, + { + name: 'Build relationships', + icon: 'users', + description: + 'Connect to supporters on a personal level and maintain accurate data.', + value: 'relationships', + }, + ]; + + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + myOption: this.items[2]['value'], + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.html b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.html new file mode 100644 index 0000000000..fac3e71216 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.html @@ -0,0 +1,7 @@ +
+ + diff --git a/libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.ts new file mode 100644 index 0000000000..58686fc75d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/basic/example.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyToggleSwitchModule } from '@skyux/forms'; + +interface ToggleSwitchFormType { + registration: FormControl; +} + +@Component({ + selector: 'app-forms-toggle-switch-basic-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyToggleSwitchModule], +}) +export class FormsToggleSwitchBasicExampleComponent { + protected formGroup: FormGroup; + protected helpPopoverContent = + 'When you open an event, a registration page becomes available online, and admins are able to register people to attend.'; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + registration: new FormControl(false), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.html new file mode 100644 index 0000000000..ae32508814 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.html @@ -0,0 +1,7 @@ +
+ + diff --git a/libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.ts new file mode 100644 index 0000000000..d8aaf97f8b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/forms/toggle-switch/help-key/example.component.ts @@ -0,0 +1,28 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyToggleSwitchModule } from '@skyux/forms'; + +interface ToggleSwitchFormType { + registration: FormControl; +} + +@Component({ + selector: 'app-forms-toggle-switch-help-key-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyToggleSwitchModule], +}) +export class FormsToggleSwitchHelpKeyExampleComponent { + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + registration: new FormControl(false), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.html b/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.html new file mode 100644 index 0000000000..8b3a7efdf7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.html @@ -0,0 +1,8 @@ +

+ Projected revenue + +

diff --git a/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.spec.ts new file mode 100644 index 0000000000..eb61625e45 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.spec.ts @@ -0,0 +1,37 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyHelpInlineHarness } from '@skyux/indicators/testing'; + +import { HelpInlineBasicExampleComponent } from './example.component'; + +describe('Basic help inline', () => { + async function setupTest(): Promise<{ + helpInlineHarness: SkyHelpInlineHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(HelpInlineBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const helpInlineHarness = await loader.getHarness( + SkyHelpInlineHarness.with({ + dataSkyId: 'help-inline-example', + }), + ); + + return { helpInlineHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HelpInlineBasicExampleComponent], + }); + }); + + it('should click the help inline button', async () => { + const { helpInlineHarness, fixture } = await setupTest(); + fixture.detectChanges(); + + const clickSpy = spyOn(fixture.componentInstance, 'onActionClick'); + await helpInlineHarness.click(); + expect(clickSpy).toHaveBeenCalled(); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.ts new file mode 100644 index 0000000000..9ec45c559d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/help-inline/basic/example.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; + +@Component({ + selector: 'app-help-inline-basic-example', + templateUrl: './example.component.html', + imports: [SkyHelpInlineModule], +}) +export class HelpInlineBasicExampleComponent { + public onActionClick(): void { + alert('Use help inline to show supplemental information to the user.'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/icon/basic/example.component.html b/libs/components/code-examples/src/lib/modules/icon/basic/example.component.html new file mode 100644 index 0000000000..a4134dfca4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/icon/basic/example.component.html @@ -0,0 +1,27 @@ + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/libs/components/code-examples/src/lib/modules/icon/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/icon/basic/example.component.spec.ts new file mode 100644 index 0000000000..79ce1b3d99 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/icon/basic/example.component.spec.ts @@ -0,0 +1,41 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyIconHarness } from '@skyux/indicators/testing'; + +import { IconBasicExampleComponent } from './example.component'; + +describe('Basic icon', () => { + async function setupTest(): Promise<{ + iconHarness: SkyIconHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(IconBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const iconHarness = await loader.getHarness( + SkyIconHarness.with({ + dataSkyId: 'icon-example', + }), + ); + + return { iconHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IconBasicExampleComponent], + }); + }); + + it('should display the correct icon', async () => { + const { iconHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(iconHarness.getIconName()).toBeResolvedTo('calendar'); + await expectAsync(iconHarness.getVariant()).toBeRejectedWithError( + 'Variant cannot be determined because variants are only assigned to icons with type `skyux`.', + ); + await expectAsync(iconHarness.getIconSize()).toBeResolvedTo('4x'); + await expectAsync(iconHarness.isFixedWidth()).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/icon/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/icon/basic/example.component.ts new file mode 100644 index 0000000000..7d69850564 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/icon/basic/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; + +@Component({ + selector: 'app-icon-basic-example', + templateUrl: './example.component.html', + imports: [SkyIconModule], +}) +export class IconBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.html b/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.html new file mode 100644 index 0000000000..336817f83b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.html @@ -0,0 +1,16 @@ + + + diff --git a/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.spec.ts new file mode 100644 index 0000000000..f151bf0581 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.spec.ts @@ -0,0 +1,48 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyIconHarness } from '@skyux/indicators/testing'; + +import { IconIconButtonExampleComponent } from './example.component'; + +describe('Icon button', () => { + async function setupTest(options: { dataSkyId?: string }): Promise<{ + iconHarness: SkyIconHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(IconIconButtonExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const iconHarness = await loader.getHarness( + SkyIconHarness.with({ + dataSkyId: options?.dataSkyId, + }), + ); + + return { iconHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IconIconButtonExampleComponent], + }); + }); + + it('should display the icon in the text icon button', async () => { + const { iconHarness, fixture } = await setupTest({ + dataSkyId: 'text-button-icon', + }); + + fixture.detectChanges(); + + await expectAsync(iconHarness.getIconName()).toBeResolvedTo('save'); + }); + + it('should display the icon in the icon only button', async () => { + const { iconHarness, fixture } = await setupTest({ + dataSkyId: 'button-icon', + }); + + fixture.detectChanges(); + + await expectAsync(iconHarness.getIconName()).toBeResolvedTo('edit'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.ts b/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.ts new file mode 100644 index 0000000000..f36c6b223b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/icon/icon-button/example.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; + +@Component({ + selector: 'app-icon-icon-button-example', + templateUrl: './example.component.html', + imports: [SkyIconModule], +}) +export class IconIconButtonExampleComponent { + protected textButtonClick(): void { + alert('Text with icon button clicked'); + } + + protected iconOnlyButtonClick(): void { + alert('Icon only button clicked'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.html new file mode 100644 index 0000000000..d9ed8cc4d8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.html @@ -0,0 +1,9 @@ + + Your password expires in {{ days }} day(s)! + diff --git a/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.spec.ts new file mode 100644 index 0000000000..71563f9532 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.spec.ts @@ -0,0 +1,56 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyAlertHarness } from '@skyux/indicators/testing'; + +import { IndicatorsAlertBasicExampleComponent } from './example.component'; + +describe('Basic alert', () => { + async function setupTest(options?: { days?: number }): Promise<{ + alertHarness: SkyAlertHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsAlertBasicExampleComponent, + ); + + if (options?.days !== undefined) { + fixture.componentInstance.days = options.days; + } + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const alertHarness = await loader.getHarness( + SkyAlertHarness.with({ dataSkyId: 'alert-example' }), + ); + + return { alertHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsAlertBasicExampleComponent], + }); + }); + + it('should show the expected alert when the number of days is 8 or more', async () => { + const { alertHarness, fixture } = await setupTest({ days: 8 }); + fixture.detectChanges(); + + await expectAsync(alertHarness.getAlertType()).toBeResolvedTo('warning'); + await expectAsync(alertHarness.getText()).toBeResolvedTo( + 'Your password expires in 8 day(s)!', + ); + await expectAsync(alertHarness.isCloseable()).toBeResolvedTo(true); + }); + + it('should show the expected alert when the number of days is 7 or fewer', async () => { + const { alertHarness, fixture } = await setupTest({ days: 7 }); + fixture.detectChanges(); + + await expectAsync(alertHarness.getAlertType()).toBeResolvedTo('danger'); + await expectAsync(alertHarness.getText()).toBeResolvedTo( + 'Your password expires in 7 day(s)!', + ); + await expectAsync(alertHarness.isCloseable()).toBeResolvedTo(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.ts new file mode 100644 index 0000000000..2a01edbf60 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/alert/basic/example.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { SkyAlertModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-alert-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyAlertModule], +}) +export class IndicatorsAlertBasicExampleComponent { + @Input() + public days = 9; + + protected onClosedChange(event: boolean): void { + alert(`Alert closed with: ${event}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.html new file mode 100644 index 0000000000..05eadeeba1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.html @@ -0,0 +1,5 @@ + diff --git a/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.spec.ts new file mode 100644 index 0000000000..cea8c300c9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.spec.ts @@ -0,0 +1,40 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyIllustrationHarness } from '@skyux/indicators/testing'; + +import { IndicatorsIllustrationBasicExampleComponent } from './example.component'; + +describe('Basic illustration', () => { + async function setupTest(): Promise<{ + illustrationHarness: SkyIllustrationHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsIllustrationBasicExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const illustrationHarness = await loader.getHarness( + SkyIllustrationHarness.with({ dataSkyId: 'illustration-example' }), + ); + + return { illustrationHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsIllustrationBasicExampleComponent], + }); + }); + + it('should display a medium-size analytics graph illustration', async () => { + const { illustrationHarness } = await setupTest(); + + await expectAsync(illustrationHarness.getName()).toBeResolvedTo( + 'analytics-graph', + ); + + await expectAsync(illustrationHarness.getSize()).toBeResolvedTo('md'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.ts new file mode 100644 index 0000000000..e5ed3f5d97 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/example.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { + SkyIllustrationModule, + SkyIllustrationResolverService, +} from '@skyux/indicators'; + +import { IllustrationDemoResolverService } from './illustration-demo-resolver.service'; + +@Component({ + selector: 'app-indicators-illustration-basic-example', + templateUrl: './example.component.html', + imports: [SkyIllustrationModule], + // This service is provided here as an example; your implementation of `SkyIllustrationResolverService` + // should be provided at the application level. + providers: [ + { + provide: SkyIllustrationResolverService, + useClass: IllustrationDemoResolverService, + }, + ], +}) +export class IndicatorsIllustrationBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/illustration-demo-resolver.service.ts b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/illustration-demo-resolver.service.ts new file mode 100644 index 0000000000..22f8627fdd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/illustration/basic/illustration-demo-resolver.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { SkyIllustrationResolverService } from '@skyux/indicators'; + +@Injectable() +export class IllustrationDemoResolverService extends SkyIllustrationResolverService { + public override resolveUrl(name: string): Promise { + const url = + name === 'analytics-graph' + ? 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yIC0yIDk2IDk2IiBoZWlnaHQ9Ijk2IiB3aWR0aD0iOTYiPjxkZWZzPjwvZGVmcz48cGF0aCBkPSJNOTAuMDgzMzMzMzMzMzMzMzQgOTAuMDgzMzMzMzMzMzMzMzRIMy44MzMzMzMzMzMzMzMzMzM1YTEuOTE2NjY2NjY2NjY2NjY2NyAxLjkxNjY2NjY2NjY2NjY2NjcgMCAwIDEgLTEuOTE2NjY2NjY2NjY2NjY2NyAtMS45MTY2NjY2NjY2NjY2NjY3VjEuOTE2NjY2NjY2NjY2NjY2NyIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik0yOC43NSA2OUE1Ljc1IDUuNzUgMCAxIDAgMjMgNjMuMjUgNS43NSA1Ljc1IDAgMCAwIDI4Ljc1IDY5WiIgZmlsbD0iIzZkZThhYiIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im02NC44NTIzMzMzMzMzMzMzMyAzMS44ODE4MzMzMzMzMzMzMzYgMTUuNjI0NjY2NjY2NjY2NjY2IC0xOS45NjAxNjY2NjY2NjY2NjYiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJtMzguMDg0MTY2NjY2NjY2NjcgMjguNTk2NjY2NjY2NjY2NjY4IDE3LjcwMjMzMzMzMzMzMzMzNSA2LjMxNzMzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im0xLjkxNjY2NjY2NjY2NjY2NjcgNDUuMTc1ODMzMzMzMzMzMzQgMjUuNzU2MTY2NjY2NjY2NjcgLTE1LjMzMzMzMzMzMzMzMzMzNCIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Im02Mi40ODMzMzMzMzMzMzMzNCA2MC4zOTggMTYuODg1ODMzMzMzMzMzMzM0IC05LjU4MzMzMzMzMzMzMzMzNCIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik0zNC41IDYzLjI1aDE3LjI1IiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PHBhdGggZD0ibTEuOTE2NjY2NjY2NjY2NjY2NyA4MC41IDIxLjkwMzY2NjY2NjY2NjY3IC0xNC4yOTgzMzMzMzMzMzMzMzQiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNMzIuNTgzMzMzMzMzMzMzMzM2IDMyLjU4MzMzMzMzMzMzMzMzNkE1Ljc1IDUuNzUgMCAxIDAgMjYuODMzMzMzMzMzMzMzMzM2IDI2LjgzMzMzMzMzMzMzMzMzNmE1Ljc1IDUuNzUgMCAwIDAgNS43NSA1Ljc1WiIgZmlsbD0iIzZkZThhYiIgc3Ryb2tlPSIjMDA0MDU0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iNCI+PC9wYXRoPjxwYXRoIGQ9Ik02MS4zMzMzMzMzMzMzMzMzMzYgNDIuMTY2NjY2NjY2NjY2NjdhNS43NSA1Ljc1IDAgMSAwIC01Ljc1IC01Ljc1QTUuNzUgNS43NSAwIDAgMCA2MS4zMzMzMzMzMzMzMzMzMzYgNDIuMTY2NjY2NjY2NjY2NjdaIiBmaWxsPSIjNmRlOGFiIiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PHBhdGggZD0iTTg0LjMzMzMzMzMzMzMzMzM0IDEzLjQxNjY2NjY2NjY2NjY2OEE1Ljc1IDUuNzUgMCAxIDAgNzguNTgzMzMzMzMzMzMzMzQgNy42NjY2NjY2NjY2NjY2NjcgNS43NSA1Ljc1IDAgMCAwIDg0LjMzMzMzMzMzMzMzMzM0IDEzLjQxNjY2NjY2NjY2NjY2OFoiIGZpbGw9IiM2ZGU4YWIiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNODQuMzMzMzMzMzMzMzMzMzQgNTMuNjY2NjY2NjY2NjY2NjdhNS43NSA1Ljc1IDAgMSAwIC01Ljc1IC01Ljc1QTUuNzUgNS43NSAwIDAgMCA4NC4zMzMzMzMzMzMzMzMzNCA1My42NjY2NjY2NjY2NjY2N1oiIGZpbGw9IiM2ZGU4YWIiIHN0cm9rZT0iIzAwNDA1NCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjQiPjwvcGF0aD48cGF0aCBkPSJNNTcuNSA2OWE1Ljc1IDUuNzUgMCAxIDAgLTUuNzUgLTUuNzVBNS43NSA1Ljc1IDAgMCAwIDU3LjUgNjlaIiBmaWxsPSIjNmRlOGFiIiBzdHJva2U9IiMwMDQwNTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI0Ij48L3BhdGg+PC9zdmc+' + : ''; + + return Promise.resolve(url); + } +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.html new file mode 100644 index 0000000000..67fd26fff6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.html @@ -0,0 +1,14 @@ + + + {{ value }} + + New members + + + This help content can add clarity and provide next steps. + diff --git a/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.spec.ts new file mode 100644 index 0000000000..311d922c39 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.spec.ts @@ -0,0 +1,72 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { SkyHelpInlineHarness } from '@skyux/help-inline/testing'; +import { SkyKeyInfoHarness } from '@skyux/indicators/testing'; + +import { IndicatorsKeyInfoBasicExampleComponent } from './example.component'; + +describe('Basic key info', () => { + async function setupTest(options?: { value?: number }): Promise<{ + keyInfoHarness: SkyKeyInfoHarness; + helpInlineHarness: SkyHelpInlineHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsKeyInfoBasicExampleComponent, + ); + + if (options?.value !== undefined) { + fixture.componentInstance.value = options.value; + } + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const keyInfoHarness = await loader.getHarness( + SkyKeyInfoHarness.with({ dataSkyId: 'key-info-example' }), + ); + const helpInlineHarness = await loader.getHarness(SkyHelpInlineHarness); + + return { keyInfoHarness, helpInlineHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsKeyInfoBasicExampleComponent], + providers: [provideNoopAnimations()], + }); + }); + + it('should display a vertical key info', async () => { + const { keyInfoHarness } = await setupTest({ value: 101 }); + + await expectAsync(keyInfoHarness.getLayout()).toBeResolvedTo('vertical'); + await expectAsync(keyInfoHarness.getValueText()).toBeResolvedTo('101'); + await expectAsync(keyInfoHarness.getLabelText()).toBeResolvedTo( + 'New members', + ); + }); + + it('should display a horizontal key info', async () => { + const { keyInfoHarness } = await setupTest({ value: 50 }); + + await expectAsync(keyInfoHarness.getLayout()).toBeResolvedTo('horizontal'); + await expectAsync(keyInfoHarness.getValueText()).toBeResolvedTo('50'); + await expectAsync(keyInfoHarness.getLabelText()).toBeResolvedTo( + 'New members', + ); + }); + + it('should include inline help', async () => { + const { helpInlineHarness } = await setupTest({ value: 50 }); + + await expectAsync(helpInlineHarness.getAriaExpanded()).toBeResolvedTo( + false, + ); + await helpInlineHarness.click(); + await expectAsync(helpInlineHarness.getAriaExpanded()).toBeResolvedTo(true); + expect(await helpInlineHarness.getPopoverContent()).toContain( + 'help content can add clarity', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.ts new file mode 100644 index 0000000000..3eb621fcb5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/key-info/basic/example.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from '@angular/core'; +import { SkyKeyInfoLayoutType, SkyKeyInfoModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-key-info-basic-example', + templateUrl: './example.component.html', + imports: [SkyKeyInfoModule], +}) +export class IndicatorsKeyInfoBasicExampleComponent { + @Input() + public set value(value: number | undefined) { + this.#_value = value; + + this.layout = + this.#_value && this.#_value >= 100 ? 'vertical' : 'horizontal'; + } + + public get value(): number | undefined { + return this.#_value; + } + + protected layout: SkyKeyInfoLayoutType = 'vertical'; + + #_value: number | undefined = 575; +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.html new file mode 100644 index 0000000000..24986cedcd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.html @@ -0,0 +1,10 @@ + + + {{ value }} + + New members + diff --git a/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.spec.ts new file mode 100644 index 0000000000..d13f40590c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.spec.ts @@ -0,0 +1,75 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyHelpInlineHarness } from '@skyux/help-inline/testing'; +import { SkyKeyInfoHarness } from '@skyux/indicators/testing'; + +import { IndicatorsKeyInfoHelpKeyExampleComponent } from './example.component'; + +describe('Basic key info', () => { + async function setupTest(options?: { value?: number }): Promise<{ + keyInfoHarness: SkyKeyInfoHarness; + helpInlineHarness: SkyHelpInlineHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + IndicatorsKeyInfoHelpKeyExampleComponent, + ); + + if (options?.value !== undefined) { + fixture.componentInstance.value = options.value; + } + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const keyInfoHarness = await loader.getHarness( + SkyKeyInfoHarness.with({ dataSkyId: 'key-info-example' }), + ); + const helpInlineHarness = await loader.getHarness(SkyHelpInlineHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { keyInfoHarness, helpInlineHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsKeyInfoHelpKeyExampleComponent, SkyHelpTestingModule], + providers: [provideNoopAnimations()], + }); + }); + + it('should display a vertical key info', async () => { + const { keyInfoHarness } = await setupTest({ value: 101 }); + + await expectAsync(keyInfoHarness.getLayout()).toBeResolvedTo('vertical'); + await expectAsync(keyInfoHarness.getValueText()).toBeResolvedTo('101'); + await expectAsync(keyInfoHarness.getLabelText()).toBeResolvedTo( + 'New members', + ); + }); + + it('should display a horizontal key info', async () => { + const { keyInfoHarness } = await setupTest({ value: 50 }); + + await expectAsync(keyInfoHarness.getLayout()).toBeResolvedTo('horizontal'); + await expectAsync(keyInfoHarness.getValueText()).toBeResolvedTo('50'); + await expectAsync(keyInfoHarness.getLabelText()).toBeResolvedTo( + 'New members', + ); + }); + + it('should have the correct help key', async () => { + const { helpInlineHarness, helpController } = await setupTest({ + value: 50, + }); + + await helpInlineHarness.click(); + + helpController.expectCurrentHelpKey('new-member-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.ts new file mode 100644 index 0000000000..5b78a2b588 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/key-info/help-key/example.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from '@angular/core'; +import { SkyKeyInfoLayoutType, SkyKeyInfoModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-key-info-help-key-example', + templateUrl: './example.component.html', + imports: [SkyKeyInfoModule], +}) +export class IndicatorsKeyInfoHelpKeyExampleComponent { + @Input() + public set value(value: number | undefined) { + this.#_value = value; + + this.layout = + this.#_value && this.#_value >= 100 ? 'vertical' : 'horizontal'; + } + + public get value(): number | undefined { + return this.#_value; + } + + protected layout: SkyKeyInfoLayoutType = 'vertical'; + + #_value: number | undefined = 575; +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.html new file mode 100644 index 0000000000..77d4efb4f6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.html @@ -0,0 +1,7 @@ + + {{ labelText }} + diff --git a/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.spec.ts new file mode 100644 index 0000000000..b3e6c0a37a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.spec.ts @@ -0,0 +1,85 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyLabelHarness } from '@skyux/indicators/testing'; + +import { IndicatorsLabelBasicExampleComponent } from './example.component'; + +describe('Basic label', () => { + async function setupTest(options?: { + daysUntilDue?: number; + submitted?: boolean; + }): Promise<{ + labelHarness: SkyLabelHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsLabelBasicExampleComponent, + ); + + if (options?.daysUntilDue !== undefined) { + fixture.componentInstance.daysUntilDue = options.daysUntilDue; + } + + if (options?.submitted !== undefined) { + fixture.componentInstance.submitted = options.submitted; + } + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const labelHarness = await loader.getHarness( + SkyLabelHarness.with({ dataSkyId: 'label-example' }), + ); + + return { labelHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsLabelBasicExampleComponent], + }); + }); + + it('should show an info label when submitted is false and there is more than a week until due', async () => { + const { labelHarness, fixture } = await setupTest({ daysUntilDue: 8 }); + fixture.detectChanges(); + + await expectAsync(labelHarness.getLabelType()).toBeResolvedTo('info'); + await expectAsync(labelHarness.getDescriptionType()).toBeResolvedTo( + 'attention', + ); + await expectAsync(labelHarness.getLabelText()).toBeResolvedTo('Incomplete'); + }); + + it('should show a warning label when submitted is false and there is less than a week until due', async () => { + const { labelHarness, fixture } = await setupTest({ daysUntilDue: 6 }); + fixture.detectChanges(); + + await expectAsync(labelHarness.getLabelType()).toBeResolvedTo('warning'); + await expectAsync(labelHarness.getDescriptionType()).toBeResolvedTo( + 'important-warning', + ); + await expectAsync(labelHarness.getLabelText()).toBeResolvedTo('Due soon'); + }); + + it('should show a danger label when submitted is false and it is past due', async () => { + const { labelHarness, fixture } = await setupTest({ daysUntilDue: -1 }); + fixture.detectChanges(); + + await expectAsync(labelHarness.getLabelType()).toBeResolvedTo('danger'); + await expectAsync(labelHarness.getDescriptionType()).toBeResolvedTo( + 'danger', + ); + await expectAsync(labelHarness.getLabelText()).toBeResolvedTo('Overdue'); + }); + + it('should show a success label when submitted is true', async () => { + const { labelHarness, fixture } = await setupTest({ submitted: true }); + fixture.detectChanges(); + + await expectAsync(labelHarness.getLabelType()).toBeResolvedTo('success'); + await expectAsync(labelHarness.getDescriptionType()).toBeResolvedTo( + 'completed', + ); + await expectAsync(labelHarness.getLabelText()).toBeResolvedTo('Submitted'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.ts new file mode 100644 index 0000000000..43ad1b0585 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/label/basic/example.component.ts @@ -0,0 +1,60 @@ +import { Component, Input } from '@angular/core'; +import { + SkyIndicatorDescriptionType, + SkyLabelModule, + SkyLabelType, +} from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-label-basic-example', + templateUrl: './example.component.html', + imports: [SkyLabelModule], +}) +export class IndicatorsLabelBasicExampleComponent { + @Input() + public get daysUntilDue(): number { + return this.#_daysUntilDue; + } + + public set daysUntilDue(days: number) { + this.#_daysUntilDue = days; + this.#updateLabelProperties(this.submitted, days); + } + + @Input() + public get submitted(): boolean { + return this.#_submitted; + } + + public set submitted(submitted: boolean) { + this.#_submitted = submitted; + this.#updateLabelProperties(submitted, this.daysUntilDue); + } + + protected descriptionType: SkyIndicatorDescriptionType = 'attention'; + protected labelText = 'Incomplete'; + protected labelType: SkyLabelType = 'info'; + + #_daysUntilDue = 14; + #_submitted = false; + + #updateLabelProperties(submitted: boolean, days: number): void { + if (submitted) { + this.labelType = 'success'; + this.descriptionType = 'completed'; + this.labelText = 'Submitted'; + } else if (days <= 0) { + this.labelType = 'danger'; + this.descriptionType = 'danger'; + this.labelText = 'Overdue'; + } else if (days <= 7) { + this.labelType = 'warning'; + this.descriptionType = 'important-warning'; + this.labelText = 'Due soon'; + } else { + this.labelType = 'info'; + this.descriptionType = 'attention'; + this.labelText = 'Incomplete'; + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.html new file mode 100644 index 0000000000..0906c9af87 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.html @@ -0,0 +1,51 @@ + + Danger status indicator + + + + Info status indicator + + + + Success status indicator + + + + Warning status indicator + + + + Warning status indicator with custom screen reader description + + + + Danger status indicator with help + diff --git a/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.spec.ts new file mode 100644 index 0000000000..62d071042f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.spec.ts @@ -0,0 +1,98 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { + SkyIndicatorDescriptionType, + SkyIndicatorIconType, +} from '@skyux/indicators'; +import { SkyStatusIndicatorHarness } from '@skyux/indicators/testing'; + +import { IndicatorsStatusIndicatorBasicExampleComponent } from './example.component'; + +describe('Status indicator basic example', () => { + async function setupTest(): Promise { + const fixture = TestBed.createComponent( + IndicatorsStatusIndicatorBasicExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + fixture.detectChanges(); + await fixture.whenStable(); + + return loader; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsStatusIndicatorBasicExampleComponent], + }); + }); + + it('should use the expected text and description type for each status indicator', async () => { + const loader = await setupTest(); + + async function validate( + dataSkyIdSuffix: string, + expectedIndicatorType: SkyIndicatorIconType, + expectedDescriptionType: SkyIndicatorDescriptionType, + expectedText: string, + expectedCustomDescription?: string, + ): Promise { + const harness = await loader.getHarness( + SkyStatusIndicatorHarness.with({ + dataSkyId: `status-indicator-${dataSkyIdSuffix}`, + }), + ); + + await expectAsync(harness.getDescriptionType()).toBeResolvedTo( + expectedDescriptionType, + ); + + await expectAsync(harness.getIndicatorType()).toBeResolvedTo( + expectedIndicatorType, + ); + + await expectAsync(harness.getText()).toBeResolvedTo(expectedText); + + if (expectedCustomDescription !== undefined) { + await expectAsync(harness.getCustomDescription()).toBeResolvedTo( + expectedCustomDescription, + ); + } + } + + await validate('error', 'danger', 'error', 'Danger status indicator'); + + await validate( + 'important-info', + 'info', + 'important-info', + 'Info status indicator', + ); + + await validate( + 'completed', + 'success', + 'completed', + 'Success status indicator', + ); + + await validate('warning', 'warning', 'warning', 'Warning status indicator'); + + await validate( + 'custom', + 'warning', + 'custom', + 'Warning status indicator with custom screen reader description', + 'Custom warning', + ); + + await validate( + 'error-with-help', + 'danger', + 'error', + 'Danger status indicator with help', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.ts new file mode 100644 index 0000000000..d4275ea2c9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/basic/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-status-indicator-basic-example', + templateUrl: './example.component.html', + imports: [SkyStatusIndicatorModule], +}) +export class IndicatorsStatusIndicatorBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.html new file mode 100644 index 0000000000..f788fe941a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.html @@ -0,0 +1,49 @@ + + Danger status indicator + + + + Info status indicator + + + + Success status indicator + + + + Warning status indicator + + + + Warning status indicator with custom screen reader description + + + + Danger status indicator with help key + diff --git a/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.spec.ts new file mode 100644 index 0000000000..6abd804228 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.spec.ts @@ -0,0 +1,119 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { + SkyIndicatorDescriptionType, + SkyIndicatorIconType, +} from '@skyux/indicators'; +import { SkyStatusIndicatorHarness } from '@skyux/indicators/testing'; + +import { IndicatorsStatusIndicatorHelpKeyExampleComponent } from './example.component'; + +describe('Status indicator basic example', () => { + async function setupTest(): Promise { + const fixture = TestBed.createComponent( + IndicatorsStatusIndicatorHelpKeyExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + fixture.detectChanges(); + await fixture.whenStable(); + + return loader; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + IndicatorsStatusIndicatorHelpKeyExampleComponent, + SkyHelpTestingModule, + ], + }); + }); + + it('should use the expected text and description type for each status indicator', async () => { + const loader = await setupTest(); + + async function validate( + dataSkyIdSuffix: string, + expectedIndicatorType: SkyIndicatorIconType, + expectedDescriptionType: SkyIndicatorDescriptionType, + expectedText: string, + expectedCustomDescription?: string, + ): Promise { + const harness = await loader.getHarness( + SkyStatusIndicatorHarness.with({ + dataSkyId: `status-indicator-${dataSkyIdSuffix}`, + }), + ); + + await expectAsync(harness.getDescriptionType()).toBeResolvedTo( + expectedDescriptionType, + ); + + await expectAsync(harness.getIndicatorType()).toBeResolvedTo( + expectedIndicatorType, + ); + + await expectAsync(harness.getText()).toBeResolvedTo(expectedText); + + if (expectedCustomDescription !== undefined) { + await expectAsync(harness.getCustomDescription()).toBeResolvedTo( + expectedCustomDescription, + ); + } + } + + await validate('error', 'danger', 'error', 'Danger status indicator'); + + await validate( + 'important-info', + 'info', + 'important-info', + 'Info status indicator', + ); + + await validate( + 'completed', + 'success', + 'completed', + 'Success status indicator', + ); + + await validate('warning', 'warning', 'warning', 'Warning status indicator'); + + await validate( + 'custom', + 'warning', + 'custom', + 'Warning status indicator with custom screen reader description', + 'Custom warning', + ); + + await validate( + 'error-with-help-key', + 'danger', + 'error', + 'Danger status indicator with help key', + ); + }); + + it('should have the correct help key', async () => { + const loader = await setupTest(); + const harness = await loader.getHarness( + SkyStatusIndicatorHarness.with({ + dataSkyId: 'status-indicator-error-with-help-key', + }), + ); + const helpController = TestBed.inject(SkyHelpTestingController); + + await harness.clickHelpInline(); + + helpController.expectCurrentHelpKey('danger-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.ts new file mode 100644 index 0000000000..d548c6d2da --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/status-indicator/help-key/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-status-indicator-help-key-example', + templateUrl: './example.component.html', + imports: [SkyStatusIndicatorModule], +}) +export class IndicatorsStatusIndicatorHelpKeyExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.html new file mode 100644 index 0000000000..31f802b127 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.html @@ -0,0 +1,19 @@ +
+ + + +
+ +
+ +
+ +
+ The text that you enter is highlighted here. + @if (showAdditionalContent) { +
This additional content is highlighted too!
+ } +
diff --git a/libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.ts new file mode 100644 index 0000000000..a4167ab4e1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/text-highlight/basic/example.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyTextHighlightModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-text-highlight-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + SkyCheckboxModule, + SkyInputBoxModule, + SkyTextHighlightModule, + ], +}) +export class IndicatorsTextHighlightBasicExampleComponent { + protected searchTerm = ''; + protected showAdditionalContent = false; +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.html new file mode 100644 index 0000000000..bb919f9320 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.spec.ts new file mode 100644 index 0000000000..e77b1599a4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.spec.ts @@ -0,0 +1,65 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyToken } from '@skyux/indicators'; +import { SkyTokensHarness } from '@skyux/indicators/testing'; + +import { IndicatorsTokensBasicExampleComponent } from './example.component'; + +describe('Tokens basic example', () => { + async function setupTest(): Promise<{ + tokensHarness: SkyTokensHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsTokensBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const tokensHarness = await loader.getHarness( + SkyTokensHarness.with({ dataSkyId: 'color-tokens' }), + ); + + return { tokensHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, IndicatorsTokensBasicExampleComponent], + }); + }); + + it('should display the expected initial tokens', async () => { + const { tokensHarness } = await setupTest(); + + await expectAsync(tokensHarness.getTokensText()).toBeResolvedTo([ + 'Red', + 'Black', + 'Blue', + 'Brown', + 'Green', + 'Orange', + 'Pink', + 'Purple', + 'Turquoise', + 'White', + 'Yellow', + ]); + }); + + it('should update the tokens array when one is dismissed', async () => { + const { tokensHarness, fixture } = await setupTest(); + + const blueToken: SkyToken<{ name: string }> = { + value: { name: 'Blue' }, + }; + + expect(fixture.componentInstance.colors).toContain(blueToken); + + await tokensHarness.dismissTokens({ + text: 'Blue', + }); + + expect(fixture.componentInstance.colors).not.toContain(blueToken); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.ts new file mode 100644 index 0000000000..730a385dfa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/tokens/basic/example.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { SkyToken, SkyTokensModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-tokens-basic-example', + templateUrl: './example.component.html', + imports: [SkyTokensModule], +}) +export class IndicatorsTokensBasicExampleComponent { + public colors: SkyToken<{ + name: string; + }>[] = [ + { value: { name: 'Red' } }, + { value: { name: 'Black' } }, + { value: { name: 'Blue' } }, + { value: { name: 'Brown' } }, + { value: { name: 'Green' } }, + { value: { name: 'Orange' } }, + { value: { name: 'Pink' } }, + { value: { name: 'Purple' } }, + { value: { name: 'Turquoise' } }, + { value: { name: 'White' } }, + { value: { name: 'Yellow' } }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.html new file mode 100644 index 0000000000..11cffa2e7c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.html @@ -0,0 +1,57 @@ +

+ These tokens define a custom property to display their values. When users + select a token, it emits an event. +

+ +
+ + (You may also include content inside the tokens component.) + +
+ +
+ + + + + + + +
+ +
+ Selected token: + {{ selectedToken }} +
diff --git a/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.spec.ts new file mode 100644 index 0000000000..d4d0a6fe60 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.spec.ts @@ -0,0 +1,120 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyTokensHarness } from '@skyux/indicators/testing'; + +import { IndicatorsTokensCustomExampleComponent } from './example.component'; + +describe('Tokens basic example', () => { + let initialTokenLabels: string[]; + + async function setupTest(): Promise<{ + tokensHarness: SkyTokensHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsTokensCustomExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const tokensHarness = await loader.getHarness( + SkyTokensHarness.with({ dataSkyId: 'example-tokens' }), + ); + + return { tokensHarness, fixture }; + } + + function clickButton( + fixture: ComponentFixture, + buttonName: 'change' | 'destroy' | 'focus-last' | 'reset', + ): void { + const btn = ( + fixture.nativeElement as HTMLElement + ).querySelector(`.tokens-example-${buttonName}-btn`); + + btn?.click(); + } + + beforeEach(() => { + initialTokenLabels = [ + 'Canada', + 'Older than 55', + 'Employed', + 'Added before 2018', + ]; + + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, IndicatorsTokensCustomExampleComponent], + }); + }); + + it('should display the expected initial tokens', async () => { + const { tokensHarness } = await setupTest(); + + await expectAsync(tokensHarness.getTokensText()).toBeResolvedTo( + initialTokenLabels, + ); + }); + + it('should display the selected token label when the user selects a token', async () => { + const { tokensHarness, fixture } = await setupTest(); + + const employedToken = (await tokensHarness.getTokens())[2]; + await employedToken.select(); + + expect( + (fixture.nativeElement as HTMLElement).querySelector( + '.tokens-example-selected', + )?.textContent, + ).toEqual('Employed'); + }); + + it('should change tokens when the user clicks the "Change tokens" button', async () => { + const { tokensHarness, fixture } = await setupTest(); + + clickButton(fixture, 'change'); + + await expectAsync(tokensHarness.getTokensText()).toBeResolvedTo([ + 'Paid', + 'Pending', + 'Past due', + ]); + }); + + it('should reset tokens when the user clicks the "Reset tokens" button', async () => { + const { tokensHarness, fixture } = await setupTest(); + + clickButton(fixture, 'change'); + + await expectAsync(tokensHarness.getTokensText()).not.toBeResolvedTo( + initialTokenLabels, + ); + + clickButton(fixture, 'reset'); + + await expectAsync(tokensHarness.getTokensText()).toBeResolvedTo( + initialTokenLabels, + ); + }); + + it('should destroy tokens when the user clicks the "Destroy tokens" button', async () => { + const { tokensHarness, fixture } = await setupTest(); + + clickButton(fixture, 'destroy'); + + await expectAsync(tokensHarness.getTokens()).toBeResolvedTo([]); + }); + + it('should focus the last token when the user clicks the "Focus last token" button', async () => { + const { tokensHarness, fixture } = await setupTest(); + + const tokens = await tokensHarness.getTokens(); + const lastToken = tokens[tokens.length - 1]; + + await expectAsync(lastToken.isFocused()).toBeResolvedTo(false); + + clickButton(fixture, 'focus-last'); + + await expectAsync(lastToken.isFocused()).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.ts new file mode 100644 index 0000000000..99a4556406 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/tokens/custom/example.component.ts @@ -0,0 +1,86 @@ +import { Component, OnDestroy } from '@angular/core'; +import { + SkyToken, + SkyTokenSelectedEventArgs, + SkyTokensMessage, + SkyTokensMessageType, + SkyTokensModule, +} from '@skyux/indicators'; + +import { Subject } from 'rxjs'; + +interface TokenItem { + label: string; +} + +@Component({ + selector: 'app-indicators-tokens-custom-example', + templateUrl: './example.component.html', + imports: [SkyTokensModule], +}) +export class IndicatorsTokensCustomExampleComponent implements OnDestroy { + protected myTokens: SkyToken[] | undefined; + protected tokensController = new Subject(); + protected selectedToken: string | undefined = ''; + + #defaultItems: TokenItem[] = [ + { label: 'Canada' }, + { label: 'Older than 55' }, + { label: 'Employed' }, + { label: 'Added before 2018' }, + ]; + + constructor() { + this.myTokens = this.#getTokens(this.#defaultItems); + } + + public ngOnDestroy(): void { + this.tokensController.complete(); + } + + protected resetTokens(): void { + this.createTokens(); + } + + protected changeTokens(): void { + this.myTokens = this.#getTokens([ + { label: 'Paid' }, + { label: 'Pending' }, + { label: 'Past due' }, + ]); + } + + protected destroyTokens(): void { + this.myTokens = undefined; + } + + protected createTokens(): void { + this.myTokens = this.#getTokens(this.#defaultItems); + } + + protected onTokenSelected(args: SkyTokenSelectedEventArgs): void { + this.selectedToken = args.token?.value.label; + } + + protected onFocusIndexUnderRange(): void { + console.log('Focus index was less than zero.'); + } + + protected onFocusIndexOverRange(): void { + console.log('Focus index was greater than the number of tokens.'); + } + + protected focusLastToken(): void { + this.tokensController.next({ + type: SkyTokensMessageType.FocusLastToken, + }); + } + + #getTokens(data: TokenItem[]): SkyToken[] { + return data.map((item) => { + return { + value: item, + }; + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.html new file mode 100644 index 0000000000..1f82662605 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.html @@ -0,0 +1,12 @@ + + +
+ The sky-wait directive can apply to a large area. + +
diff --git a/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.spec.ts new file mode 100644 index 0000000000..c38c01f6de --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.spec.ts @@ -0,0 +1,42 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyWaitHarness } from '@skyux/indicators/testing'; + +import { IndicatorsWaitElementExampleComponent } from './example.component'; + +describe('Basic wait', () => { + async function setupTest(): Promise<{ + waitHarness: SkyWaitHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + IndicatorsWaitElementExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + const waitHarness = await loader.getHarness( + SkyWaitHarness.with({ dataSkyId: 'wait-example' }), + ); + + return { waitHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsWaitElementExampleComponent], + }); + }); + + it('should show the wait component when the user performs an action', async () => { + const { waitHarness, fixture } = await setupTest(); + + (fixture.nativeElement as HTMLElement) + .querySelector('.sky-btn') + ?.click(); + + fixture.detectChanges(); + + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(true); + await expectAsync(waitHarness.isFullPage()).toBeResolvedTo(false); + await expectAsync(waitHarness.isNonBlocking()).toBeResolvedTo(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.ts new file mode 100644 index 0000000000..c6c32413c2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/wait/element/example.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SkyWaitModule } from '@skyux/indicators'; + +@Component({ + selector: 'app-indicators-wait-element-example', + templateUrl: './example.component.html', + imports: [SkyWaitModule], +}) +export class IndicatorsWaitElementExampleComponent { + protected isWaiting = false; +} diff --git a/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.html b/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.html new file mode 100644 index 0000000000..fe9fb442eb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.html @@ -0,0 +1,15 @@ + + + diff --git a/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.spec.ts new file mode 100644 index 0000000000..0dda9b61ad --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.spec.ts @@ -0,0 +1,58 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyWaitHarness } from '@skyux/indicators/testing'; + +import { IndicatorsWaitPageExampleComponent } from './example.component'; + +describe('Page wait', () => { + function setupTest(): { + rootLoader: HarnessLoader; + el: HTMLElement; + fixture: ComponentFixture; + } { + const fixture = TestBed.createComponent(IndicatorsWaitPageExampleComponent); + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const el = fixture.nativeElement as HTMLElement; + + return { rootLoader, el, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [IndicatorsWaitPageExampleComponent], + }); + }); + + it('should show the page wait component when the user performs an action', async () => { + const { rootLoader, el } = setupTest(); + + const buttons = el.querySelectorAll('.sky-btn'); + + buttons[0].click(); + + const waitHarness = await rootLoader.getHarness( + SkyWaitHarness.with({ servicePageWaitType: 'blocking' }), + ); + + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(true); + await expectAsync(waitHarness.isFullPage()).toBeResolvedTo(true); + await expectAsync(waitHarness.isNonBlocking()).toBeResolvedTo(false); + }); + + it('should show the non-blocking page wait component when the user performs an action', async () => { + const { rootLoader, el } = setupTest(); + + const buttons = el.querySelectorAll('.sky-btn'); + + buttons[1].click(); + + const waitHarness = await rootLoader.getHarness( + SkyWaitHarness.with({ servicePageWaitType: 'non-blocking' }), + ); + + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(true); + await expectAsync(waitHarness.isFullPage()).toBeResolvedTo(true); + await expectAsync(waitHarness.isNonBlocking()).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.ts b/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.ts new file mode 100644 index 0000000000..8fb1d0db16 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/indicators/wait/page/example.component.ts @@ -0,0 +1,33 @@ +import { Component, OnDestroy, inject } from '@angular/core'; +import { SkyWaitService } from '@skyux/indicators'; + +@Component({ + standalone: true, + selector: 'app-indicators-wait-page-example', + templateUrl: './example.component.html', +}) +export class IndicatorsWaitPageExampleComponent implements OnDestroy { + protected isWaiting = false; + + readonly #waitSvc = inject(SkyWaitService); + + public ngOnDestroy(): void { + this.#waitSvc.dispose(); + } + + protected togglePageWait(isBlocking: boolean): void { + if (!this.isWaiting) { + if (isBlocking) { + this.#waitSvc.beginBlockingPageWait(); + } else { + this.#waitSvc.beginNonBlockingPageWait(); + } + } else if (isBlocking) { + this.#waitSvc.endBlockingPageWait(); + } else { + this.#waitSvc.endNonBlockingPageWait(); + } + + this.isWaiting = !this.isWaiting; + } +} diff --git a/libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.html b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.html new file mode 100644 index 0000000000..35b9e8dce3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.html @@ -0,0 +1,33 @@ + + + + + + + +
First name: + {{ firstName }} + + +
+
+ + +
+ + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.ts new file mode 100644 index 0000000000..3b992428fd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/basic/example.component.ts @@ -0,0 +1,66 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; +import { + SkyInlineFormButtonLayout, + SkyInlineFormCloseArgs, + SkyInlineFormConfig, + SkyInlineFormModule, +} from '@skyux/inline-form'; + +interface DemoForm { + firstName: FormControl; +} + +@Component({ + selector: 'app-inline-form-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyIconModule, + SkyInlineFormModule, + SkyInputBoxModule, + ], +}) +export class InlineFormBasicExampleComponent { + protected firstName = 'Jane'; + protected formGroup: FormGroup; + + protected inlineFormConfig: SkyInlineFormConfig = { + buttonLayout: SkyInlineFormButtonLayout.SaveCancel, + }; + + protected showForm = false; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + firstName: new FormControl('', { nonNullable: true }), + }); + } + + protected onInlineFormClose(args: SkyInlineFormCloseArgs): void { + if (args.reason === 'save') { + this.firstName = this.formGroup.value.firstName ?? ''; + } + + this.showForm = false; + this.formGroup.patchValue({ + firstName: undefined, + }); + } + + protected onInlineFormOpen(): void { + this.showForm = true; + this.formGroup.patchValue({ + firstName: this.firstName, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.html b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.html new file mode 100644 index 0000000000..004db6ffe1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.html @@ -0,0 +1,33 @@ + + + + + + + +
First name: + {{ firstName }} + + +
+
+ + +
+ + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.ts b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.ts new file mode 100644 index 0000000000..d277cd25c7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/custom-buttons/example.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; +import { + SkyInlineFormButtonLayout, + SkyInlineFormCloseArgs, + SkyInlineFormConfig, + SkyInlineFormModule, +} from '@skyux/inline-form'; + +interface DemoForm { + firstName: FormControl; +} + +@Component({ + selector: 'app-inline-form-custom-buttons-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyIconModule, + SkyInlineFormModule, + SkyInputBoxModule, + ], +}) +export class InlineFormCustomButtonsExampleComponent implements OnInit { + protected firstName = 'Jane'; + protected formGroup: FormGroup; + + protected inlineFormConfig: SkyInlineFormConfig = { + buttonLayout: SkyInlineFormButtonLayout.Custom, + buttons: [ + { + action: 'save', + text: 'Save', + styleType: 'primary', + }, + { + action: 'clear', + text: 'Clear', + styleType: 'default', + }, + { + action: 'reset', + text: 'Reset', + styleType: 'default', + }, + { + action: 'cancel', + text: 'Cancel', + styleType: 'link', + }, + ], + }; + + protected showForm = false; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + firstName: new FormControl('', { nonNullable: true }), + }); + } + + public ngOnInit(): void { + this.formGroup.valueChanges.subscribe(() => { + if ( + this.inlineFormConfig.buttons && + this.inlineFormConfig.buttons[0].disabled !== this.formGroup.invalid + ) { + this.inlineFormConfig.buttons[0].disabled = this.formGroup.invalid; + this.inlineFormConfig = { ...{}, ...this.inlineFormConfig }; + } + }); + } + + protected onInlineFormClose(args: SkyInlineFormCloseArgs): void { + switch (args.reason) { + case 'save': + this.firstName = this.formGroup.value.firstName ?? ''; + this.showForm = false; + break; + + case 'clear': + this.formGroup.patchValue({ firstName: '' }); + break; + + case 'reset': + this.formGroup.setValue({ firstName: this.firstName }); + break; + + default: + this.showForm = false; + break; + } + } + + protected onInlineFormOpen(): void { + this.showForm = true; + this.formGroup.patchValue({ + firstName: this.firstName, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.html b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.html new file mode 100644 index 0000000000..34bcf9d678 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.html @@ -0,0 +1,41 @@ + + @for (item of items; track item) { + + +
+ {{ item.title }} +
+
+ + + + + + {{ item.note }} + +
+ } +
+ + +
+ + + + + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.ts b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.ts new file mode 100644 index 0000000000..4da3481a71 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/inline-form/inline-form/repeaters/example.component.ts @@ -0,0 +1,108 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; +import { + SkyInlineFormButtonLayout, + SkyInlineFormCloseArgs, + SkyInlineFormConfig, +} from '@skyux/inline-form'; +import { SkyRepeaterModule } from '@skyux/lists'; + +interface DemoForm { + id: FormControl; + note: FormControl; + title: FormControl; +} + +interface Item { + id: string; + title: string | undefined; + note: string | undefined; +} + +@Component({ + selector: 'app-inline-form-repeaters-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyIconModule, + SkyInputBoxModule, + SkyRepeaterModule, + ], +}) +export class InlineFormRepeatersExampleComponent { + protected activeInlineFormId: string | undefined; + protected formGroup: FormGroup; + + protected inlineFormConfig: SkyInlineFormConfig = { + buttonLayout: SkyInlineFormButtonLayout.SaveCancel, + }; + + protected items: Item[] = [ + { + id: '1', + title: '2019 Spring Gala', + note: 'Gala for friends and family', + }, + { + id: '2', + title: '2019 Special Winter Event', + note: 'A special event', + }, + { + id: '3', + title: '2019 Donor Appreciation Event', + note: 'Event for all donors and families', + }, + { + id: '4', + title: '2020 Spring Gala', + note: 'Gala for friends and family', + }, + ]; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + id: new FormControl('', { nonNullable: true }), + title: new FormControl('', { nonNullable: true }), + note: new FormControl('', { nonNullable: true }), + }); + } + + protected showInlineForm(item: Item): void { + this.activeInlineFormId = item.id; + this.formGroup.patchValue({ + note: item.note, + title: item.title, + }); + } + + protected onInlineFormClose(args: SkyInlineFormCloseArgs): void { + if (args.reason === 'save') { + const found = this.items.find( + (item) => item.id === this.activeInlineFormId, + ); + + if (found) { + found.note = this.formGroup.value.note; + found.title = this.formGroup.value.title; + } + } + + this.formGroup.patchValue({ + note: undefined, + title: undefined, + }); + + // Close the active form. + this.activeInlineFormId = undefined; + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.html b/libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.html new file mode 100644 index 0000000000..684c6a7815 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.html @@ -0,0 +1,17 @@ + + + + Build a new list + + Start from scratch and fine-tune with filters. + + + + + + Open a saved list + + Open a list with filters saved in the web view. + + + diff --git a/libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.ts new file mode 100644 index 0000000000..f5a7e6c217 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/action-button/basic/example.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { SkyActionButtonModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-action-button-basic-example', + templateUrl: './example.component.html', + imports: [SkyActionButtonModule], +}) +export class LayoutActionButtonBasicExampleComponent { + protected filterActionClick(): void { + alert('Filter action clicked'); + } + + protected openActionClick(): void { + alert('Open action clicked'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.html b/libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.html new file mode 100644 index 0000000000..ce3f8ac042 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.html @@ -0,0 +1,13 @@ + + + + Open a link + Open a link. + + + + + Open a router link + Open a router link. + + diff --git a/libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.ts new file mode 100644 index 0000000000..3ead9a8545 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/action-button/permalink/example.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { SkyActionButtonModule, SkyActionButtonPermalink } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-action-button-permalink-example', + templateUrl: './example.component.html', + imports: [SkyActionButtonModule], +}) +export class LayoutActionButtonPermalinkExampleComponent { + protected routerlink: SkyActionButtonPermalink = { + route: { + commands: [], + extras: { + queryParams: { + component: 'MyComponent', + }, + }, + }, + }; + + protected url: SkyActionButtonPermalink = { + url: 'https://www.stackblitz.com', + }; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.html b/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.html new file mode 100644 index 0000000000..12aa06416b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.html @@ -0,0 +1,13 @@ + + @for (person of personList; track person) { + + + {{ person.name }} + + + {{ person.address }} + + + } + + diff --git a/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.ts new file mode 100644 index 0000000000..69dd8fea81 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/example.component.ts @@ -0,0 +1,164 @@ +import { Component, OnInit } from '@angular/core'; +import { SkyBackToTopModule } from '@skyux/layout'; +import { SkyInfiniteScrollModule, SkyRepeaterModule } from '@skyux/lists'; + +import { Person } from './person'; + +@Component({ + selector: 'app-layout-back-to-top-infinite-scroll-example', + templateUrl: './example.component.html', + imports: [SkyBackToTopModule, SkyInfiniteScrollModule, SkyRepeaterModule], +}) +export class LayoutBackToTopInfiniteScrollExampleComponent implements OnInit { + public hasMore = true; + + public personList: Person[] = []; + + public personDataSet = [ + { + name: 'Barbara Durr', + address: '7436 Fieldstone Court', + }, + { + name: 'Colton Chamberlain', + address: '342 Foster Court', + }, + { + name: 'Alva Clifford', + address: '657 West Rockville Street', + }, + { + name: 'Tonja Sanderson', + address: '7004 Third Street', + }, + { + name: 'Paulene Baum', + address: '9309 Mammoth Street', + }, + { + name: 'Jessy Witherspoon', + address: '43 Canal Street', + }, + { + name: 'Solomon Hurley', + address: '667 Wakehurst Circle', + }, + { + name: 'Calandra Geer', + address: '164 Applegate Drive', + }, + { + name: 'Latrice Ashmore', + address: '7965 Lake Road', + }, + { + name: 'Rod Tomlinson', + address: '9664 Newport Drive', + }, + { + name: 'Cristen Sizemore', + address: '17 Edgefield Street', + }, + { + name: 'Kristeen Lunsford', + address: '245 Green Lake Street', + }, + { + name: 'Jack Lovett', + address: '73 Academy Street', + }, + { + name: 'Elwood Farris', + address: '90 Smoky Hollow Court', + }, + { + name: 'Ilene Woo', + address: '71 Pumpkin Hill Street', + }, + { + name: 'Kanesha Hutto', + address: '107 East Cooper Street', + }, + { + name: 'Nick Bourne', + address: '62 Evergreen Street', + }, + { + name: 'Ed Sipes', + address: '8824 Hill Street', + }, + { + name: 'Wonda Lumpkin', + address: '134 North Warren Street', + }, + { + name: 'Cheyenne Lightfoot', + address: '184 Pierce Avenue', + }, + { + name: 'Darcel Lenz', + address: '9408 Beechwood Street', + }, + { + name: 'Martine Rocha', + address: '871 Shadow Brook Street', + }, + { + name: 'Cherelle Connell', + address: '649 Glenwood Street', + }, + { + name: 'Molly Seymour', + address: '386 E. George Street', + }, + { + name: 'Clarice Overton', + address: '16 Manchester Street', + }, + { + name: 'Eliza Vanhorn', + address: '8232 S. Augusta Street', + }, + ]; + + public ngOnInit(): void { + void this.#addData(0, 5); + } + + public onScrollEnd(): void { + void this.#addData(this.personList.length, 5); + } + + async #addData(start: number, rowSize: number): Promise { + if (this.hasMore) { + const result = await this.mockRemote(start, rowSize); + this.personList = this.personList.concat(result.data); + this.hasMore = result.hasMore; + } + } + + /** + * Simulate a remote request. + */ + private mockRemote( + start: number, + rowSize: number, + ): Promise<{ data: Person[]; hasMore: boolean }> { + const data: Person[] = []; + + for (let i = 0; i < rowSize; i++) { + if (this.personDataSet[start + i]) { + data.push(this.personDataSet[start + i]); + } + } + + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + data, + hasMore: this.personList.length < this.personDataSet.length, + }); + }, 1000); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/person.ts b/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/person.ts new file mode 100644 index 0000000000..5c96225783 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/back-to-top/infinite-scroll/person.ts @@ -0,0 +1,4 @@ +export interface Person { + name: string; + address: string; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.html b/libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.html new file mode 100644 index 0000000000..3249180ac5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.html @@ -0,0 +1,12 @@ + + @for (person of personList; track person) { + + + {{ person.name }} + + + {{ person.address }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.ts new file mode 100644 index 0000000000..868d6444e3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/back-to-top/repeater/example.component.ts @@ -0,0 +1,117 @@ +import { Component } from '@angular/core'; +import { SkyBackToTopModule } from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; + +@Component({ + selector: 'app-layout-back-to-top-repeater-example', + templateUrl: './example.component.html', + imports: [SkyBackToTopModule, SkyRepeaterModule], +}) +export class LayoutBackToTopRepeaterExampleComponent { + public personList = [ + { + name: 'Barbara Durr', + address: '7436 Fieldstone Court', + }, + { + name: 'Colton Chamberlain', + address: '342 Foster Court', + }, + { + name: 'Alva Clifford', + address: '657 West Rockville Street', + }, + { + name: 'Tonja Sanderson', + address: '7004 Third Street', + }, + { + name: 'Paulene Baum', + address: '9309 Mammoth Street', + }, + { + name: 'Jessy Witherspoon', + address: '43 Canal Street', + }, + { + name: 'Solomon Hurley', + address: '667 Wakehurst Circle', + }, + { + name: 'Calandra Geer', + address: '164 Applegate Drive', + }, + { + name: 'Latrice Ashmore', + address: '7965 Lake Road', + }, + { + name: 'Rod Tomlinson', + address: '9664 Newport Drive', + }, + { + name: 'Cristen Sizemore', + address: '17 Edgefield Street', + }, + { + name: 'Kristeen Lunsford', + address: '245 Green Lake Street', + }, + { + name: 'Jack Lovett', + address: '73 Academy Street', + }, + { + name: 'Elwood Farris', + address: '90 Smoky Hollow Court', + }, + { + name: 'Ilene Woo', + address: '71 Pumpkin Hill Street', + }, + { + name: 'Kanesha Hutto', + address: '107 East Cooper Street', + }, + { + name: 'Nick Bourne', + address: '62 Evergreen Street', + }, + { + name: 'Ed Sipes', + address: '8824 Hill Street', + }, + { + name: 'Wonda Lumpkin', + address: '134 North Warren Street', + }, + { + name: 'Cheyenne Lightfoot', + address: '184 Pierce Avenue', + }, + { + name: 'Darcel Lenz', + address: '9408 Beechwood Street', + }, + { + name: 'Martine Rocha', + address: '871 Shadow Brook Street', + }, + { + name: 'Cherelle Connell', + address: '649 Glenwood Street', + }, + { + name: 'Molly Seymour', + address: '386 E. George Street', + }, + { + name: 'Clarice Overton', + address: '16 Manchester Street', + }, + { + name: 'Eliza Vanhorn', + address: '8232 S. Augusta Street', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.html b/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.html new file mode 100644 index 0000000000..97c670ee80 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + +

Box content

+
+
diff --git a/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.spec.ts new file mode 100644 index 0000000000..4d18c0e025 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.spec.ts @@ -0,0 +1,36 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyBoxHarness } from '@skyux/layout/testing'; + +import { LayoutBoxBasicExampleComponent } from './example.component'; + +describe('Basic box', () => { + async function setupTest(): Promise<{ + boxHarness: SkyBoxHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(LayoutBoxBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const boxHarness = await loader.getHarness( + SkyBoxHarness.with({ + dataSkyId: 'box-example', + }), + ); + + return { boxHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LayoutBoxBasicExampleComponent], + }); + }); + + it('should display the correct box', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(boxHarness.getAriaLabel()).toBeResolvedTo('Box header'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.ts new file mode 100644 index 0000000000..a76890953b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/box/basic/example.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SkyBoxModule } from '@skyux/layout'; +import { SkyDropdownModule } from '@skyux/popovers'; + +@Component({ + selector: 'app-layout-box-basic-example', + templateUrl: './example.component.html', + imports: [SkyBoxModule, SkyDropdownModule], +}) +export class LayoutBoxBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.html new file mode 100644 index 0000000000..5c7e374c56 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + +

Box content

+
+
diff --git a/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.spec.ts new file mode 100644 index 0000000000..98bf5e259c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.spec.ts @@ -0,0 +1,51 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyBoxHarness } from '@skyux/layout/testing'; + +import { LayoutBoxHelpKeyExampleComponent } from './example.component'; + +describe('Basic box', () => { + async function setupTest(): Promise<{ + boxHarness: SkyBoxHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent(LayoutBoxHelpKeyExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const boxHarness = await loader.getHarness( + SkyBoxHarness.with({ + dataSkyId: 'box-example', + }), + ); + + const helpController = TestBed.inject(SkyHelpTestingController); + + return { boxHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LayoutBoxHelpKeyExampleComponent, SkyHelpTestingModule], + }); + }); + + it('should display the correct box', async () => { + const { boxHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(boxHarness.getAriaLabel()).toBeResolvedTo('Box header'); + }); + + it('should have the correct help key', async () => { + const { boxHarness, helpController } = await setupTest(); + + await boxHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('box-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.ts new file mode 100644 index 0000000000..7f3c8dd18e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/box/help-key/example.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SkyBoxModule } from '@skyux/layout'; +import { SkyDropdownModule } from '@skyux/popovers'; + +@Component({ + selector: 'app-layout-box-help-key-example', + templateUrl: './example.component.html', + imports: [SkyBoxModule, SkyDropdownModule], +}) +export class LayoutBoxHelpKeyExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.html b/libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.html new file mode 100644 index 0000000000..2e560442c1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.html @@ -0,0 +1,62 @@ + + @if (showTitle) { + Large card + } + @if (showContent) { + + This card examplenstrates the amount of space that is available for a card + that uses the default large size. If the content does not require this + much space, you can set the card size to small. The type of content to + display in the body of a card varies based on what the card represents and + whether it prompts users to action. + + } + @if (showAction) { + + + + } + + + @if (showTitle) { + Small card + } + @if (showContent) { + + This card examplenstrates the reduced amount of space that is available + when you set the card size to small. + + } + @if (showAction) { + + + + + + + + + + } + + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
diff --git a/libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.ts new file mode 100644 index 0000000000..17740c9e57 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/card/basic/example.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { SkyCardModule } from '@skyux/layout'; +import { SkyDropdownModule } from '@skyux/popovers'; + +@Component({ + selector: 'app-layout-card-basic-example', + templateUrl: './example.component.html', + imports: [FormsModule, SkyCardModule, SkyCheckboxModule, SkyDropdownModule], +}) +export class LayoutCardBasicExampleComponent { + protected showAction = true; + protected showCheckbox = true; + protected showContent = true; + protected showTitle = true; + + protected triggerAlert(): void { + alert('Action clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.html b/libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.html new file mode 100644 index 0000000000..b2fd2cee7d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.html @@ -0,0 +1,15 @@ + + + Definition list heading + + @for (item of items; track item) { + + + {{ item.label }} + + + {{ item.value }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.ts new file mode 100644 index 0000000000..6543e788f9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/definition-list/basic/example.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { SkyDefinitionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-definition-list-basic-example', + templateUrl: './example.component.html', + imports: [SkyDefinitionListModule], +}) +export class LayoutDefinitionListBasicExampleComponent { + protected items: { label: string; value?: string }[] = [ + { + label: 'Field 1', + value: 'Field 1 value', + }, + { + label: 'Field 2', + value: 'Field 2 value', + }, + { + label: 'Field 3', + value: undefined, + }, + { + label: 'Field 4', + value: 'Field 4 value', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.html new file mode 100644 index 0000000000..c39e63ad3d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.html @@ -0,0 +1,12 @@ + + @for (item of items; track item) { + + + {{ item.term }} + + + {{ item.description }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.ts new file mode 100644 index 0000000000..c641cadd80 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/help-key/example.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-description-list-help-key-example', + templateUrl: './example.component.html', + imports: [SkyDescriptionListModule], +}) +export class LayoutDescriptionListHelpKeyExampleComponent { + protected items: { term: string; description: string; helpKey?: string }[] = [ + { + term: 'College', + description: 'Humanities and Social Sciences', + helpKey: 'college-help', + }, + { + term: 'Department', + description: 'Anthropology', + }, + { + term: 'Advisor', + description: 'Cathy Green', + }, + { + term: 'Class year', + description: '2024', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.html b/libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.html new file mode 100644 index 0000000000..71b885c74a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.html @@ -0,0 +1,12 @@ + + @for (item of items; track item) { + + + {{ item.term }} + + + {{ item.description }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.ts new file mode 100644 index 0000000000..18565828ea --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/horizontal/example.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-description-list-horizontal-example', + templateUrl: './example.component.html', + imports: [SkyDescriptionListModule], +}) +export class LayoutDescriptionListHorizontalExampleComponent { + protected items: { term: string; description: string; helpText?: string }[] = + [ + { + term: 'College', + description: 'Humanities and Social Sciences', + }, + { + term: 'Department', + description: 'Anthropology', + }, + { + term: 'Advisor', + description: 'Cathy Green', + helpText: 'The faculty member who advises the student.', + }, + { + term: 'Class year', + description: '2024', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.html b/libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.html new file mode 100644 index 0000000000..b6588433a0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.html @@ -0,0 +1,17 @@ + + @for (item of items; track item) { + + + {{ item.term }} + + + + {{ item.description }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.ts new file mode 100644 index 0000000000..fa59b2082d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/inline-help/example.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; +import { SkyDescriptionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-description-list-inline-help-example', + templateUrl: './example.component.html', + imports: [SkyDescriptionListModule, SkyHelpInlineModule], +}) +export class LayoutDescriptionListInlineHelpExampleComponent { + protected items: { term: string; description: string; helpText?: string }[] = + [ + { + term: 'College', + description: 'Humanities and Social Sciences', + }, + { + term: 'Department', + description: 'Anthropology', + }, + { + term: 'Advisor', + description: 'Cathy Green', + helpText: 'The faculty member who advises the student.', + }, + { + term: 'Class year', + description: '2024', + }, + ]; + + protected onActionClick(): void { + alert('Help inline button clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.html b/libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.html new file mode 100644 index 0000000000..9a2e079a0f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.html @@ -0,0 +1,12 @@ + + @for (item of items; track item) { + + + {{ item.term }} + + + {{ item.description }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.ts new file mode 100644 index 0000000000..c7d7bc2d29 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/long-description/example.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-description-list-long-description-example', + templateUrl: './example.component.html', + imports: [SkyDescriptionListModule], +}) +export class LayoutDescriptionListLongDescriptionExampleComponent { + protected items: { term: string; description: string }[] = [ + { + term: 'Good Health and Well-being', + description: + 'Ensure healthy lives and promote well-being for all at all ages.', + }, + { + term: 'Quality Education', + description: + 'Ensure inclusive and equitable quality education and promote lifelong learning opportunities for all.', + }, + { + term: 'Gender Equity', + description: 'Achieve gender equality and empower all women and girls.', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.html b/libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.html new file mode 100644 index 0000000000..a43aeb89a8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.html @@ -0,0 +1,12 @@ + + @for (item of items; track item) { + + + {{ item.term }} + + + {{ item.description }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.ts new file mode 100644 index 0000000000..79565e0dc2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/description-list/vertical/example.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-description-list-vertical-example', + templateUrl: './example.component.html', + imports: [SkyDescriptionListModule], +}) +export class LayoutDescriptionListVerticalExampleComponent { + protected items: { term: string; description: string; helpText?: string }[] = + [ + { + term: 'College', + description: 'Humanities and Social Sciences', + }, + { + term: 'Department', + description: 'Anthropology', + }, + { + term: 'Advisor', + description: 'Cathy Green', + helpText: 'The faculty member who advises the student.', + }, + { + term: 'Class year', + description: '2024', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.html b/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.html new file mode 100644 index 0000000000..7ffd49ecfa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.html @@ -0,0 +1,104 @@ +
+ + + + [screenSmall]="1" + + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + [screenSmall]="1" + + + + [screenSmall]="2" + [screenSmall]="2" + [screenSmall]="2" + [screenSmall]="2" + [screenSmall]="2" + [screenSmall]="2" + + + + [screenSmall]="3" + [screenSmall]="3" + [screenSmall]="3" + [screenSmall]="3" + + + + [screenSmall]="4" + [screenSmall]="4" + [screenSmall]="4" + + + + [screenSmall]="5" + [screenSmall]="7" + + + + [screenSmall]="6" + [screenSmall]="6" + + + + [screenSmall]="8" + [screenSmall]="4" + + + + [screenSmall]="9" + [screenSmall]="3" + + + + [screenSmall]="10" + [screenSmall]="2" + + + + [screenSmall]="11" + [screenSmall]="1" + + + + + [screenXSmall]="6" [screenSmall]="8" [screenMedium]="9" + [screenLarge]="10" + + + [screenXSmall]="6" [screenSmall]="4" [screenMedium]="3" + [screenLarge]="2" + + + + + First column + Second column + Third column + + +
diff --git a/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.spec.ts new file mode 100644 index 0000000000..afd432131d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.spec.ts @@ -0,0 +1,128 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyColumnHarness, + SkyFluidGridHarness, + SkyRowHarness, +} from '@skyux/layout/testing'; + +import { LayoutFluidGridExampleComponent } from './example.component'; + +describe('Basic fluid grid', () => { + async function setupTest(): Promise<{ + fluidGridHarness: SkyFluidGridHarness; + fixture: ComponentFixture; + loader: HarnessLoader; + }> { + const fixture = TestBed.createComponent(LayoutFluidGridExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const fluidGridHarness = await loader.getHarness( + SkyFluidGridHarness.with({ + dataSkyId: 'fluid-grid', + }), + ); + + return { fluidGridHarness, fixture, loader }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LayoutFluidGridExampleComponent], + }); + }); + + it('should display the correct fluid grid', async () => { + const { fluidGridHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + const rows = await fluidGridHarness.getRows(); + + expect(rows.length).toEqual(12); + }); + + it('should indicate the grid has margins', async () => { + const { fluidGridHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(fluidGridHarness.hasMargin()).toBeResolvedTo(true); + + fixture.componentInstance.disableMargin = true; + fixture.detectChanges(); + + await expectAsync(fluidGridHarness.hasMargin()).toBeResolvedTo(false); + }); + + it('should get the gutter size', async () => { + const { fluidGridHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(fluidGridHarness.getGutterSize()).toBeResolvedTo('large'); + + fixture.componentInstance.gutterSize = 'medium'; + fixture.detectChanges(); + + await expectAsync(fluidGridHarness.getGutterSize()).toBeResolvedTo( + 'medium', + ); + + fixture.componentInstance.gutterSize = 'small'; + fixture.detectChanges(); + + await expectAsync(fluidGridHarness.getGutterSize()).toBeResolvedTo('small'); + }); + + it('should get the correct row harness', async () => { + const { fixture, loader } = await setupTest(); + const rowHarness = await loader.getHarness( + SkyRowHarness.with({ dataSkyId: 'test-row' }), + ); + + fixture.detectChanges(); + + const columns = await rowHarness.getColumns(); + + expect(columns.length).toEqual(12); + await expectAsync(rowHarness.getColumnOrder()).toBeResolvedTo('normal'); + }); + + it('should get the row direction from the row harness', async () => { + const { fixture, loader } = await setupTest(); + const rowHarness = await loader.getHarness( + SkyRowHarness.with({ dataSkyId: 'reverse-row' }), + ); + + fixture.detectChanges(); + + await expectAsync(rowHarness.getColumnOrder()).toBeResolvedTo('reverse'); + }); + + it('should get the correct column harness', async () => { + const { fixture, loader } = await setupTest(); + const columnHarness = await loader.getHarness( + SkyColumnHarness.with({ dataSkyId: 'test-column' }), + ); + + fixture.detectChanges(); + + await expectAsync(columnHarness.getXSmallSize()).toBeResolvedTo(12); + await expectAsync(columnHarness.getLargeSize()).toBeResolvedTo(1); + }); + + it('should get the column sizes from the column harness', async () => { + const { fixture, loader } = await setupTest(); + const columnHarness = await loader.getHarness( + SkyColumnHarness.with({ dataSkyId: 'dynamic-column' }), + ); + + fixture.detectChanges(); + + await expectAsync(columnHarness.getXSmallSize()).toBeResolvedTo(6); + await expectAsync(columnHarness.getSmallSize()).toBeResolvedTo(8); + await expectAsync(columnHarness.getMediumSize()).toBeResolvedTo(9); + await expectAsync(columnHarness.getLargeSize()).toBeResolvedTo(10); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.ts new file mode 100644 index 0000000000..ae2c7b2a8e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/fluid-grid/example.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { SkyFluidGridGutterSizeType, SkyFluidGridModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-fluid-grid-example', + templateUrl: './example.component.html', + styles: [ + ` + .highlight-columns .sky-column { + background-color: #97eced; + border: 1px solid #56e0e1; + overflow-wrap: break-word; + } + `, + ], + imports: [SkyFluidGridModule], +}) +export class LayoutFluidGridExampleComponent { + public gutterSize: SkyFluidGridGutterSizeType | undefined; + public disableMargin = false; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/format/example.component.html b/libs/components/code-examples/src/lib/modules/layout/format/example.component.html new file mode 100644 index 0000000000..82180237cf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/format/example.component.html @@ -0,0 +1,12 @@ + + + + 39,210 + + + + 78 + diff --git a/libs/components/code-examples/src/lib/modules/layout/format/example.component.scss b/libs/components/code-examples/src/lib/modules/layout/format/example.component.scss new file mode 100644 index 0000000000..308ce54e0c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/format/example.component.scss @@ -0,0 +1,7 @@ +.number-large { + font-size: 18px; +} + +.number-medium { + font-size: 16px; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/format/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/format/example.component.ts new file mode 100644 index 0000000000..89c71b0957 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/format/example.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SkyFormatModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-format-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + imports: [SkyFormatModule], +}) +export class LayoutFormatExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.html b/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.html new file mode 100644 index 0000000000..841cd8cba8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.html @@ -0,0 +1,19 @@ +
+ Custom content +
+ +
+ @if (deleting) { + + } +
diff --git a/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.scss b/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.scss new file mode 100644 index 0000000000..9110532a64 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.scss @@ -0,0 +1,15 @@ +.inline-delete-container { + padding: 15px; + background-color: white; + border: black solid 1px; + height: 400px; + width: 400px; + position: relative; +} + +.inline-delete-trigger { + position: absolute; + bottom: 10px; + left: 0; + width: 100%; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.ts new file mode 100644 index 0000000000..03961ff86f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/inline-delete/custom/example.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyInlineDeleteModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-inline-delete-custom-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + imports: [SkyIconModule, SkyInlineDeleteModule], +}) +export class LayoutInlineDeleteCustomExampleComponent { + protected deleting = false; + protected pending = false; + + protected deleteItem(): void { + this.deleting = true; + } + + protected onCancelTriggered(): void { + this.deleting = false; + } + + protected onDeleteTriggered(): void { + setTimeout(() => { + this.pending = false; + this.deleting = false; + + alert( + 'Custom element deletion was triggered. In a real scenario the item would be removed. Item was not removed just for example purposes.', + ); + }, 3000); + + this.pending = true; + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.html b/libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.html new file mode 100644 index 0000000000..5d222c58aa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.html @@ -0,0 +1,38 @@ + + @for (item of repeaterDemoItems; track item) { + + + + + + + + + + + +
+ {{ item.title }} +
+
+ {{ item.cost }} +
+
+ + {{ item.description }} + + @if (repeaterDemoShownInlineDeletes.indexOf(item.title) >= 0) { + + } +
+ } +
+ + diff --git a/libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.ts new file mode 100644 index 0000000000..c2d72611a3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/inline-delete/repeater/example.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { SkyInlineDeleteModule } from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyDropdownModule } from '@skyux/popovers'; + +interface InlineRepeaterDemoItem { + title: string; + cost: string; + description: string; +} + +@Component({ + selector: 'app-layout-inline-delete-repeater-example', + templateUrl: './example.component.html', + imports: [SkyDropdownModule, SkyInlineDeleteModule, SkyRepeaterModule], +}) +export class LayoutInlineDeleteRepeaterExampleComponent { + protected originalRepeaterDemoItems: InlineRepeaterDemoItem[] = [ + { + title: 'Individual', + cost: '$75.00', + description: '1 ticket', + }, + { + title: 'Foursome', + cost: '$250.00', + description: '4 tickets', + }, + { + title: 'Hole sponsor', + cost: '$1,000.00', + description: '8 tickets', + }, + ]; + + protected repeaterDemoItems = this.originalRepeaterDemoItems; + protected repeaterDemoShownInlineDeletes: string[] = []; + + protected showInlineDelete(title: string): void { + this.repeaterDemoShownInlineDeletes.push(title); + } + + protected deleteItem(title: string): void { + this.repeaterDemoItems = this.repeaterDemoItems.filter( + (exampleItem: InlineRepeaterDemoItem) => exampleItem.title !== title, + ); + + this.repeaterDemoShownInlineDeletes = + this.repeaterDemoShownInlineDeletes.filter( + (exampleItem: string) => exampleItem !== title, + ); + } + + protected cancelDeletion(title: string): void { + this.repeaterDemoShownInlineDeletes = + this.repeaterDemoShownInlineDeletes.filter( + (exampleItem: string) => exampleItem !== title, + ); + } + + protected onResetClick(): void { + this.repeaterDemoItems = this.originalRepeaterDemoItems; + this.repeaterDemoShownInlineDeletes = []; + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.html b/libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.html new file mode 100644 index 0000000000..ba78032a19 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.html @@ -0,0 +1,74 @@ + + @if (showAlert) { + + This is an alert. + + } + @if (showImage) { + + + + } + @if (showTitle) { + + {{ name }} + + } + @if (showSubtitle) { + Board member + } + @if (showStatus) { + + Fundraiser + Inactive + + } + @if (showContent) { + + This is the arbitrary content section. You can display any kind of content + to complement the content on a page. We recommend that you display content + to support the key tasks of users of users who visit the page. We also + recommend that you keep in mind the context of how users will use the + content and limit the amount of content to avoid overloading the summary. + + } + @if (showKeyInfo) { + + + $1,500 + Largest gift + + + 37 + Total gifts + + + } + + +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
diff --git a/libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.ts new file mode 100644 index 0000000000..aab39b0abe --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/page-summary/basic/example.component.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyAvatarModule } from '@skyux/avatar'; +import { SkyCheckboxModule } from '@skyux/forms'; +import { + SkyAlertModule, + SkyKeyInfoModule, + SkyLabelModule, +} from '@skyux/indicators'; +import { SkyPageSummaryModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-page-summary-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + SkyAlertModule, + SkyAvatarModule, + SkyCheckboxModule, + SkyKeyInfoModule, + SkyLabelModule, + SkyPageSummaryModule, + ], +}) +export class LayoutPageSummaryBasicExampleComponent { + protected name = 'Robert C. Hernandez'; + protected showAlert = true; + protected showContent = true; + protected showImage = true; + protected showKeyInfo = true; + protected showStatus = true; + protected showSubtitle = true; + protected showTitle = true; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.html b/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.html new file mode 100644 index 0000000000..1c35377d7f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.html @@ -0,0 +1,33 @@ + +
+
+ + + +

Fit layout example

+
+
+ + +

+ Elements in a page with a layout of "fit" can be absolutely + positioned inside it. This is especially powerful when combined + with content that uses + CSS flexbox. This example uses flexbox to display a header with a variable + height and content that fills the rest of the available viewport. +

+
+
+
+
+
+
Left
+
Top
+
Right
+
Bottom
+
Center
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.scss b/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.scss new file mode 100644 index 0000000000..0b32b5a093 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.scss @@ -0,0 +1,49 @@ +.layout-fit-example { + display: flex; + flex-direction: column; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + + &-header { + flex-grow: 0; + } + + &-content { + background-color: var(--sky-background-color-info-light); + flex-grow: 1; + position: relative; + } +} + +.anchored-item { + position: absolute; + padding: var(--sky-padding-even-md); + + &-left { + left: 0; + top: 50%; + } + + &-top { + top: 0; + left: 50%; + } + + &-right { + top: 50%; + right: 0; + } + + &-bottom { + bottom: 0; + left: 50%; + } + + &-center { + top: 50%; + left: 50%; + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.ts new file mode 100644 index 0000000000..f4c5ce8264 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/page/layout-fit/example.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SkyFluidGridModule, SkyPageModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-page-layout-fit-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + imports: [SkyFluidGridModule, SkyPageModule], +}) +export class LayoutPageLayoutFitExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.html b/libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.html new file mode 100644 index 0000000000..7a546dd3ed --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.ts new file mode 100644 index 0000000000..e9cf85e691 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand-repeater/example.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { SkyTextExpandRepeaterModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-text-expand-repeater-example', + templateUrl: './example.component.html', + imports: [SkyTextExpandRepeaterModule], +}) +export class LayoutTextExpandRepeaterExampleComponent { + protected repeaterData: string[] = [ + 'Repeater item 1', + 'Repeater item 2', + 'Repeater item 3', + 'Repeater item 4', + 'Repeater item 5', + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.html b/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.html new file mode 100644 index 0000000000..6b320a7d91 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.spec.ts new file mode 100644 index 0000000000..4b641c29db --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.spec.ts @@ -0,0 +1,57 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyTextExpandHarness } from '@skyux/layout/testing'; + +import { LayoutTextExpandInlineExampleComponent } from './example.component'; + +describe('Text expand inline example', () => { + async function setupTest( + options: { + dataSkyId?: string; + } = {}, + ): Promise<{ + textExpandHarness: SkyTextExpandHarness; + }> { + await TestBed.configureTestingModule({ + imports: [LayoutTextExpandInlineExampleComponent, NoopAnimationsModule], + }).compileComponents(); + + const fixture = TestBed.createComponent( + LayoutTextExpandInlineExampleComponent, + ); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const textExpandHarness: SkyTextExpandHarness = options.dataSkyId + ? await loader.getHarness( + SkyTextExpandHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyTextExpandHarness); + + return { textExpandHarness }; + } + + it('should set up the text expand', async () => { + const { textExpandHarness } = await setupTest(); + + await expectAsync(textExpandHarness.textExpandsToModal()).toBeResolvedTo( + false, + ); + + await textExpandHarness.clickExpandCollapseButton(); + + await expectAsync(textExpandHarness.getText()).toBeResolvedTo( + 'The text expand component truncates long blocks of text with an ellipsis and a link to expand the text. Users select the link to expand the full text inline unless it exceeds limits on text characters or newline characters. If the text exceeds those limits, then it expands in a modal view instead. The component does not truncate text that is shorter than a specified threshold, and by default, it removes newline characters from truncated text.', + ); + await expectAsync(textExpandHarness.isExpanded()).toBeResolvedTo(true); + + await textExpandHarness.clickExpandCollapseButton(); + + await expectAsync(textExpandHarness.getText()).toBeResolvedTo( + 'The text expand component truncates long blocks of text with an ellipsis and a link to expand the text. Users select the link to expand the full text inline unless it exceeds limits on text characters', + ); + await expectAsync(textExpandHarness.isExpanded()).toBeResolvedTo(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.ts new file mode 100644 index 0000000000..0f01c1a490 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/inline/example.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { SkyTextExpandModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-text-expand-inline-example', + templateUrl: './example.component.html', + imports: [SkyTextExpandModule], +}) +export class LayoutTextExpandInlineExampleComponent { + protected longText = + 'The text expand component truncates long blocks of text with an ellipsis and a link to expand the text. Users select the link to expand the full text inline unless it exceeds limits on text characters or newline characters. If the text exceeds those limits, then it expands in a modal view instead. The component does not truncate text that is shorter than a specified threshold, and by default, it removes newline characters from truncated text.'; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.html b/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.html new file mode 100644 index 0000000000..072b95d89e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.spec.ts new file mode 100644 index 0000000000..6ac505dbd5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.spec.ts @@ -0,0 +1,62 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyTextExpandHarness } from '@skyux/layout/testing'; + +import { LayoutTextExpandModalExampleComponent } from './example.component'; + +describe('Text expand modal example', () => { + async function setupTest( + options: { + dataSkyId?: string; + } = {}, + ): Promise<{ + textExpandHarness: SkyTextExpandHarness; + }> { + await TestBed.configureTestingModule({ + imports: [LayoutTextExpandModalExampleComponent, NoopAnimationsModule], + }).compileComponents(); + + const fixture = TestBed.createComponent( + LayoutTextExpandModalExampleComponent, + ); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const textExpandHarness: SkyTextExpandHarness = options.dataSkyId + ? await loader.getHarness( + SkyTextExpandHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyTextExpandHarness); + + return { textExpandHarness }; + } + + it('should open and close the text expand modal', async () => { + const { textExpandHarness } = await setupTest(); + + await expectAsync(textExpandHarness.textExpandsToModal()).toBeResolvedTo( + true, + ); + await expectAsync(textExpandHarness.isExpanded()).toBeResolvedTo(false); + + await textExpandHarness.clickExpandCollapseButton(); + const modal = await textExpandHarness.getExpandedViewModal(); + + await expectAsync(modal.getText()).toBeResolvedTo( + 'The text expand component truncates long blocks of text with an ellipsis and a link to expand the text. Users select the link to expand the full text inline unless it exceeds limits on text characters or newline characters. If the text exceeds those limits, then it expands in a modal view instead. The component does not truncate text that is shorter than a specified threshold, and by default, it removes newline characters from truncated text.', + ); + await expectAsync(modal.getExpandModalTitle()).toBeResolvedTo( + 'Expanded view', + ); + await expectAsync(textExpandHarness.isExpanded()).toBeResolvedTo(true); + + await modal.clickCloseButton(); + + await expectAsync( + textExpandHarness.getExpandedViewModal(), + ).toBeRejectedWithError('Could not find text expand modal.'); + await expectAsync(textExpandHarness.isExpanded()).toBeResolvedTo(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.ts new file mode 100644 index 0000000000..cf9ffc3424 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/modal/example.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { SkyTextExpandModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-text-expand-modal-example', + templateUrl: './example.component.html', + imports: [SkyTextExpandModule], +}) +export class LayoutTextExpandModalExampleComponent { + protected longText = + 'The text expand component truncates long blocks of text with an ellipsis and a link to expand the text. Users select the link to expand the full text inline unless it exceeds limits on text characters or newline characters. If the text exceeds those limits, then it expands in a modal view instead. The component does not truncate text that is shorter than a specified threshold, and by default, it removes newline characters from truncated text.'; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.html b/libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.html new file mode 100644 index 0000000000..cb4e65e624 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.ts new file mode 100644 index 0000000000..59116061da --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/text-expand/newline/example.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { SkyTextExpandModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-text-expand-newline-example', + templateUrl: './example.component.html', + imports: [SkyTextExpandModule], +}) +export class LayoutTextExpandNewlineExampleComponent { + protected newlinesText = + 'The text expand component truncates long blocks of text with an ellipsis and a link to expand the text.\nUsers select the link to expand the full text inline unless it exceeds limits on text characters or newline characters.\nIf the text exceeds those limits, then it expands in a modal view instead.\nThe component does not truncate text that is shorter than a specified threshold, and by default, it removes newline characters from truncated text.'; +} diff --git a/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.html b/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.html new file mode 100644 index 0000000000..db9a1c8ed5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.spec.ts new file mode 100644 index 0000000000..8ff52e6a3c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.spec.ts @@ -0,0 +1,52 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyToolbarHarness } from '@skyux/layout/testing'; + +import { LayoutToolbarBasicExampleComponent } from './example.component'; + +describe('Basic toolbar example', () => { + async function setupTest( + options: { + dataSkyId?: string; + } = {}, + ): Promise<{ + toolbarHarness: SkyToolbarHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + imports: [LayoutToolbarBasicExampleComponent], + }).compileComponents(); + + const fixture = TestBed.createComponent(LayoutToolbarBasicExampleComponent); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const toolbarHarness: SkyToolbarHarness = options.dataSkyId + ? await loader.getHarness( + SkyToolbarHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyToolbarHarness); + + return { toolbarHarness, fixture }; + } + + it('should set up the toolbar', async () => { + const { toolbarHarness, fixture } = await setupTest(); + + const toolbarItem1 = await toolbarHarness.getItem({ + dataSkyId: 'toolbar-item-1', + }); + const toolbarButton = await toolbarItem1.querySelector('button'); + + const viewActionsHarness = await toolbarHarness.getViewActions(); + const expandButton = ( + await viewActionsHarness.querySelectorAll('button') + )[0]; + + const clickSpy = spyOn(fixture.componentInstance, 'onButtonClicked'); + await toolbarButton?.click(); + await expandButton.click(); + expect(clickSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.ts new file mode 100644 index 0000000000..a69fe03ac0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/toolbar/basic/example.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyToolbarModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-toolbar-basic-example', + templateUrl: './example.component.html', + imports: [SkyIconModule, SkyToolbarModule], +}) +export class LayoutToolbarBasicExampleComponent { + public onButtonClicked(buttonText: string): void { + alert(buttonText + ' clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.html b/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.html new file mode 100644 index 0000000000..1aeb293d3d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.html @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.spec.ts new file mode 100644 index 0000000000..4d9c2ad516 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.spec.ts @@ -0,0 +1,58 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyToolbarHarness } from '@skyux/layout/testing'; + +import { LayoutToolbarSectionedExampleComponent } from './example.component'; + +describe('Sectioned toolbar example', () => { + async function setupTest( + options: { + dataSkyId?: string; + } = {}, + ): Promise<{ + toolbarHarness: SkyToolbarHarness; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + imports: [LayoutToolbarSectionedExampleComponent], + }).compileComponents(); + + const fixture = TestBed.createComponent( + LayoutToolbarSectionedExampleComponent, + ); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const toolbarHarness: SkyToolbarHarness = options.dataSkyId + ? await loader.getHarness( + SkyToolbarHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyToolbarHarness); + + return { toolbarHarness, fixture }; + } + + it('should set up the toolbar', async () => { + const { toolbarHarness, fixture } = await setupTest(); + + const toolbarSectionHarness = await toolbarHarness.getSection({ + dataSkyId: 'top-section', + }); + + const toolbarItem1 = await toolbarSectionHarness.getItem({ + dataSkyId: 'toolbar-item-1', + }); + const toolbarButton = await toolbarItem1.querySelector('button'); + + const viewActionsHarness = await toolbarSectionHarness.getViewActions(); + const expandButton = ( + await viewActionsHarness.querySelectorAll('button') + )[0]; + + const clickSpy = spyOn(fixture.componentInstance, 'onButtonClicked'); + await toolbarButton?.click(); + await expandButton.click(); + expect(clickSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.ts b/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.ts new file mode 100644 index 0000000000..c7e2e6d2d1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/layout/toolbar/sectioned/example.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyToolbarModule } from '@skyux/layout'; + +@Component({ + selector: 'app-layout-toolbar-sectioned-example', + templateUrl: './example.component.html', + imports: [SkyIconModule, SkyToolbarModule], +}) +export class LayoutToolbarSectionedExampleComponent { + public onButtonClicked(buttonText: string): void { + alert(buttonText + ' clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.html b/libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.html new file mode 100644 index 0000000000..923c6faa60 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.html @@ -0,0 +1,44 @@ + + + + + + + + +
+ + + + + + + + + + +
+ + + @for (item of filteredItems; track item) { + + + {{ item.name }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.ts new file mode 100644 index 0000000000..c3b02c7c16 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/inline/example.component.ts @@ -0,0 +1,150 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkyFilterModule, SkyRepeaterModule } from '@skyux/lists'; + +interface Filter { + name: string; + value: string | boolean; +} + +interface Fruit { + name: string; + type: string; + color: string; +} + +@Component({ + selector: 'app-lists-filter-inline-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + SkyCheckboxModule, + SkyIdModule, + SkyFilterModule, + SkyInputBoxModule, + SkyRepeaterModule, + SkyToolbarModule, + ], +}) +export class ListsFilterInlineExampleComponent { + protected appliedFilters: Filter[] = []; + protected filteredItems: Fruit[]; + protected filtersActive = false; + protected fruitType = 'any'; + protected hideOrange = false; + + protected items: Fruit[] = [ + { + name: 'Orange', + type: 'citrus', + color: 'orange', + }, + { + name: 'Mango', + type: 'other', + color: 'orange', + }, + { + name: 'Lime', + type: 'citrus', + color: 'green', + }, + { + name: 'Strawberry', + type: 'berry', + color: 'red', + }, + { + name: 'Blueberry', + type: 'berry', + color: 'blue', + }, + ]; + + protected showInlineFilters = false; + + constructor() { + this.filteredItems = this.items.slice(); + } + + protected filterButtonClicked(): void { + this.showInlineFilters = !this.showInlineFilters; + } + + protected fruitTypeChange(newValue: string): void { + this.fruitType = newValue; + this.#setFilterActiveState(); + } + + protected hideOrangeChange(newValue: boolean): void { + this.hideOrange = newValue; + this.#setFilterActiveState(); + } + + #setFilterActiveState(): void { + this.appliedFilters = []; + + if (this.fruitType !== 'any') { + this.appliedFilters.push({ + name: 'fruitType', + value: this.fruitType, + }); + } + + if (this.hideOrange) { + this.appliedFilters.push({ + name: 'hideOrange', + value: true, + }); + } + + this.filtersActive = this.appliedFilters.length > 0; + this.filteredItems = this.#filterItems(this.items, this.appliedFilters); + } + + #orangeFilterFailed(filter: Filter, item: Fruit): boolean { + return ( + filter.name === 'hideOrange' && !!filter.value && item.color === 'orange' + ); + } + + #fruitTypeFilterFailed(filter: Filter, item: Fruit): boolean { + return ( + filter.name === 'fruitType' && + filter.value !== 'any' && + filter.value !== item.type + ); + } + + #itemIsShown(filters: Filter[], item: Fruit): boolean { + let passesFilter = true, + j: number; + + for (j = 0; j < filters.length; j++) { + if (this.#orangeFilterFailed(filters[j], item)) { + passesFilter = false; + } else if (this.#fruitTypeFilterFailed(filters[j], item)) { + passesFilter = false; + } + } + + return passesFilter; + } + + #filterItems(items: Fruit[], filters: Filter[]): Fruit[] { + let i: number, passesFilter: boolean; + const result: Fruit[] = []; + + for (i = 0; i < items.length; i++) { + passesFilter = this.#itemIsShown(filters, items[i]); + if (passesFilter) { + result.push(items[i]); + } + } + + return result; + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.html b/libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.html new file mode 100644 index 0000000000..a96de50086 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.html @@ -0,0 +1,30 @@ + + + + + + + @if (appliedFilters && appliedFilters.length > 0) { + + + @for (item of appliedFilters; track item; let i = $index) { + + {{ item.label }} + + } + + + } + + + @for (item of filteredItems; track item) { + + + {{ item.name }} + + + } + diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.ts new file mode 100644 index 0000000000..ca4d48274c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/example.component.ts @@ -0,0 +1,132 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkyFilterModule, SkyRepeaterModule } from '@skyux/lists'; +import { SkyModalCloseArgs, SkyModalService } from '@skyux/modals'; + +import { Filter } from './filter'; +import { FilterModalContext } from './filter-modal-context'; +import { FilterModalComponent } from './filter-modal.component'; +import { Fruit } from './fruit'; + +@Component({ + selector: 'app-lists-filter-modal-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyFilterModule, SkyRepeaterModule, SkyToolbarModule], +}) +export class ListsFilterModalExampleComponent { + protected appliedFilters: Filter[] = []; + protected filteredItems: Fruit[]; + protected items: Fruit[] = [ + { + name: 'Orange', + type: 'citrus', + color: 'orange', + }, + { + name: 'Mango', + type: 'other', + color: 'orange', + }, + { + name: 'Lime', + type: 'citrus', + color: 'green', + }, + { + name: 'Strawberry', + type: 'berry', + color: 'red', + }, + { + name: 'Blueberry', + type: 'berry', + color: 'blue', + }, + ]; + + protected showInlineFilters = false; + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #modalSvc = inject(SkyModalService); + + constructor() { + this.filteredItems = this.items.slice(); + } + + protected onDismiss(index: number): void { + this.appliedFilters.splice(index, 1); + this.filteredItems = this.#filterItems(this.items, this.appliedFilters); + } + + protected onInlineFilterButtonClicked(): void { + this.showInlineFilters = !this.showInlineFilters; + } + + protected onModalFilterButtonClick(): void { + const modalInstance = this.#modalSvc.open(FilterModalComponent, [ + { + provide: FilterModalContext, + useValue: { + appliedFilters: this.appliedFilters, + }, + }, + ]); + + modalInstance.closed.subscribe((result: SkyModalCloseArgs) => { + if (result.reason === 'save') { + this.appliedFilters = (result.data as Filter[]).slice(); + this.filteredItems = this.#filterItems(this.items, this.appliedFilters); + this.#changeDetectorRef.markForCheck(); + } + }); + } + + #fruitTypeFilterFailed(filter: Filter, item: Fruit): boolean { + return ( + filter.name === 'fruitType' && + filter.value !== 'any' && + filter.value !== item.type + ); + } + + #itemIsShown(filters: Filter[], item: Fruit): boolean { + let passesFilter = true, + j: number; + + for (j = 0; j < filters.length; j++) { + if (this.#orangeFilterFailed(filters[j], item)) { + passesFilter = false; + } else if (this.#fruitTypeFilterFailed(filters[j], item)) { + passesFilter = false; + } + } + + return passesFilter; + } + + #filterItems(items: Fruit[], filters: Filter[]): Fruit[] { + let i: number, passesFilter: boolean; + const result: Fruit[] = []; + + for (i = 0; i < items.length; i++) { + passesFilter = this.#itemIsShown(filters, items[i]); + if (passesFilter) { + result.push(items[i]); + } + } + + return result; + } + + #orangeFilterFailed(filter: Filter, item: Fruit): boolean { + return ( + filter.name === 'hideOrange' && !!filter.value && item.color === 'orange' + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal-context.ts b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal-context.ts new file mode 100644 index 0000000000..64d6ab4d9b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal-context.ts @@ -0,0 +1,5 @@ +import { Filter } from './filter'; + +export class FilterModalContext { + public appliedFilters: Filter[] = []; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.html b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.html new file mode 100644 index 0000000000..329e8f8936 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.ts new file mode 100644 index 0000000000..fcea91bc48 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter-modal.component.ts @@ -0,0 +1,76 @@ +import { Component, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { Filter } from './filter'; +import { FilterModalContext } from './filter-modal-context'; + +@Component({ + selector: 'app-filter-modal', + templateUrl: './filter-modal.component.html', + imports: [FormsModule, SkyCheckboxModule, SkyInputBoxModule, SkyModalModule], +}) +export class FilterModalComponent { + protected hideOrange = false; + protected fruitType = 'any'; + + protected readonly context = inject(FilterModalContext); + protected readonly instance = inject(SkyModalInstance); + + constructor() { + if (this.context.appliedFilters.length > 0) { + this.#setFormFilters(this.context.appliedFilters); + } else { + this.clearAllFilters(); + } + } + + protected applyFilters(): void { + const result = this.#getAppliedFiltersArray(); + this.instance.save(result); + } + + protected cancel(): void { + this.instance.cancel(); + } + + protected clearAllFilters(): void { + this.hideOrange = false; + this.fruitType = 'any'; + } + + #getAppliedFiltersArray(): Filter[] { + const appliedFilters: Filter[] = []; + + if (this.fruitType !== 'any') { + appliedFilters.push({ + name: 'fruitType', + value: this.fruitType, + label: this.fruitType, + }); + } + + if (this.hideOrange) { + appliedFilters.push({ + name: 'hideOrange', + value: true, + label: 'hide orange fruits', + }); + } + + return appliedFilters; + } + + #setFormFilters(appliedFilters: Filter[]): void { + for (const appliedFilter of appliedFilters) { + if (appliedFilter.name === 'fruitType') { + this.fruitType = `${appliedFilter.value}`; + } + + if (appliedFilter.name === 'hideOrange') { + this.hideOrange = !!appliedFilter.value; + } + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter.ts b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter.ts new file mode 100644 index 0000000000..b02ecacdba --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/filter.ts @@ -0,0 +1,5 @@ +export interface Filter { + name: string; + value: string | boolean; + label: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/filter/modal/fruit.ts b/libs/components/code-examples/src/lib/modules/lists/filter/modal/fruit.ts new file mode 100644 index 0000000000..ae7cbfeb16 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/filter/modal/fruit.ts @@ -0,0 +1,5 @@ +export interface Fruit { + name: string; + type: string; + color: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.html b/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.html new file mode 100644 index 0000000000..034ad3dce4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.html @@ -0,0 +1,12 @@ +
+ + @for (item of items; track item) { + + + {{ item.name }} + + + } + + +
diff --git a/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.ts new file mode 100644 index 0000000000..6eb6b7067b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/example.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { SkyInfiniteScrollModule, SkyRepeaterModule } from '@skyux/lists'; + +import { InfiniteScrollDemoItem } from './item'; + +let nextId = 0; + +@Component({ + selector: 'app-lists-infinite-scroll-repeater-example', + templateUrl: './example.component.html', + imports: [SkyInfiniteScrollModule, SkyRepeaterModule], +}) +export class ListsInfiniteScrollRepeaterExampleComponent implements OnInit { + protected items: InfiniteScrollDemoItem[] = []; + protected itemsHaveMore = true; + + public ngOnInit(): void { + void this.#addData(); + } + + protected onScrollEnd(): void { + if (this.itemsHaveMore) { + void this.#addData(); + } + } + + async #addData(): Promise { + const result = await this.#mockRemote(); + this.items = this.items.concat(result.data); + this.itemsHaveMore = result.hasMore; + } + + #mockRemote(): Promise<{ + data: InfiniteScrollDemoItem[]; + hasMore: boolean; + }> { + const data: InfiniteScrollDemoItem[] = []; + + for (let i = 0; i < 8; i++) { + data.push({ + name: `Item #${++nextId}`, + }); + } + + // Simulate async request. + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + data, + hasMore: nextId < 50, + }); + }, 1000); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/item.ts b/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/item.ts new file mode 100644 index 0000000000..58f6e0786c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/infinite-scroll/repeater/item.ts @@ -0,0 +1,3 @@ +export interface InfiniteScrollDemoItem { + name: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.html new file mode 100644 index 0000000000..5b7df4fcd6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.html @@ -0,0 +1,8 @@ + + +

The current page is {{ currentPage }}.

diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.ts new file mode 100644 index 0000000000..6fe890ced8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/basic/example.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SkyPagingModule } from '@skyux/lists'; + +@Component({ + selector: 'app-lists-paging-basic-example', + templateUrl: './example.component.html', + imports: [SkyPagingModule], +}) +export class ListsPagingBasicExampleComponent { + protected currentPage = 1; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.service.ts b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.service.ts new file mode 100644 index 0000000000..11b589a918 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; + +import { Observable, delay, of } from 'rxjs'; + +import { DemoData } from './demo-data'; + +const people = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoDataService { + public getPagedData( + pageNumber: number, + itemCount: number, + ): Observable { + const startIndex = (pageNumber - 1) * itemCount; + + return of({ + people: people.slice(startIndex, startIndex + itemCount), + totalCount: people.length, + }).pipe( + // Simulate network latency. + delay(1000), + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.ts b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.ts new file mode 100644 index 0000000000..8a53e0e014 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/demo-data.ts @@ -0,0 +1,6 @@ +import { Person } from './person'; + +export interface DemoData { + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.html b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.html new file mode 100644 index 0000000000..bd1ee3c576 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.html @@ -0,0 +1,31 @@ + + + + @for (person of (pagedData | async)?.people; track person) { + + + {{ person.name }} + + + + + + Formal name + + + {{ person.formal }} + + + + + + } + + + diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.ts new file mode 100644 index 0000000000..eee313f2ea --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/example.component.ts @@ -0,0 +1,42 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyDescriptionListModule } from '@skyux/layout'; +import { + SkyPagingContentChangeArgs, + SkyPagingModule, + SkyRepeaterModule, +} from '@skyux/lists'; + +import { Subject, shareReplay, switchMap, tap } from 'rxjs'; + +import { DemoDataService } from './demo-data.service'; + +@Component({ + selector: 'app-lists-paging-with-content-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + SkyDescriptionListModule, + SkyPagingModule, + SkyRepeaterModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ListsPagingWithContentExampleComponent { + #demoDataSvc = inject(DemoDataService); + + protected currentPage = 1; + protected pageSize = 5; + protected contentChange = new Subject(); + + protected pagedData = this.contentChange.pipe( + switchMap((args) => + this.#demoDataSvc.getPagedData(args.currentPage, this.pageSize).pipe( + tap(() => { + args.loadingComplete(); + }), + ), + ), + shareReplay(1), + ); +} diff --git a/libs/components/code-examples/src/lib/modules/lists/paging/with-content/person.ts b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/person.ts new file mode 100644 index 0000000000..74d4c1c096 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/paging/with-content/person.ts @@ -0,0 +1,4 @@ +export interface Person { + name: string; + formal: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.html b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.html new file mode 100644 index 0000000000..9dac3c6516 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.html @@ -0,0 +1,62 @@ +
+ + @for (item of items; track item) { + + +
+ {{ item.title }} +
+
+ {{ item.status }} +
+
+ + + + + + + + + + + + + + {{ item.note }} + +
+ } +
+
+ + + + diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.scss b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.scss new file mode 100644 index 0000000000..48aa422544 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.scss @@ -0,0 +1,8 @@ +.example-repeater-flex { + display: flex; + flex-wrap: wrap; + + .example-repeater-item-title { + flex-grow: 1; + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.spec.ts new file mode 100644 index 0000000000..51ef49dcb6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.spec.ts @@ -0,0 +1,143 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyRepeaterHarness } from '@skyux/lists/testing'; + +import { ListsRepeaterAddRemoveExampleComponent } from './example.component'; + +describe('Repeater add remove example', () => { + async function setupTest(): Promise<{ + el: HTMLElement; + fixture: ComponentFixture; + repeaterHarness: SkyRepeaterHarness; + }> { + const fixture = TestBed.createComponent( + ListsRepeaterAddRemoveExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const repeaterHarness = await loader.getHarness( + SkyRepeaterHarness.with({ dataSkyId: 'repeater-example' }), + ); + + const el = fixture.nativeElement as HTMLElement; + + return { el, fixture, repeaterHarness }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ListsRepeaterAddRemoveExampleComponent, NoopAnimationsModule], + }); + }); + + it('should allow items to be expanded and collapsed', async () => { + const { repeaterHarness } = await setupTest(); + + const repeaterItems = await repeaterHarness.getRepeaterItems(); + + let first = true; + + for (const item of repeaterItems) { + await expectAsync(item.isCollapsible()).toBeResolvedTo(true); + + // in single expand mode, the first item is expanded by default + await expectAsync(item.isExpanded()).toBeResolvedTo(first ? true : false); + + first = false; + + await item.collapse(); + await expectAsync(item.isExpanded()).toBeResolvedTo(false); + + await item.expand(); + await expectAsync(item.isExpanded()).toBeResolvedTo(true); + } + }); + + it('should allow items to be reordered', async () => { + const { repeaterHarness } = await setupTest(); + + const expectedContent = [ + { + title: 'Call Robert Hernandez Completed', + body: 'Robert recently gave a very generous gift. We should call him to thank him.', + }, + { + title: 'Send invitation to Spring Ball Past due', + body: "The Spring Ball is coming up soon. Let's get those invitations out!", + }, + { + title: 'Assign prospects Due tomorrow', + body: 'There are 14 new prospects who are not assigned to fundraisers.', + }, + { + title: 'Process gift receipts Due next week', + body: 'There are 28 recent gifts that are not receipted.', + }, + ]; + + let repeaterItems = await repeaterHarness.getRepeaterItems(); + + expect(repeaterItems).toBeDefined(); + expect(repeaterItems.length).toBe(expectedContent.length); + + for (const item of repeaterItems) { + await expectAsync(item.isReorderable()).toBeResolvedTo(true); + } + + await expectAsync(repeaterItems[1].getTitleText()).toBeResolvedTo( + expectedContent[1].title, + ); + + await repeaterItems[1].sendToTop(); + repeaterItems = await repeaterHarness.getRepeaterItems(); + + await expectAsync(repeaterItems[1].getTitleText()).toBeResolvedTo( + expectedContent[0].title, + ); + }); + + it('should allow items to be added and removed', async () => { + const { repeaterHarness, el, fixture } = await setupTest(); + + let repeaterItems = await repeaterHarness.getRepeaterItems(); + + expect(repeaterItems).toBeDefined(); + expect(repeaterItems.length).toBe(4); + + for (const item of repeaterItems) { + await expectAsync(item.isSelectable()).toBeResolvedTo(true); + } + + const addButton = el.querySelector( + '[data-sky-id="add-button"]', + ); + + const removeButton = el.querySelector( + '[data-sky-id="remove-button"]', + ); + + addButton?.click(); + fixture.detectChanges(); + + repeaterItems = await repeaterHarness.getRepeaterItems(); + expect(repeaterItems).toBeDefined(); + expect(repeaterItems.length).toBe(5); + + await expectAsync(repeaterItems[0].isSelected()).toBeResolvedTo(false); + await repeaterItems[0].select(); + + await expectAsync(repeaterItems[0].isSelected()).toBeResolvedTo(true); + await expectAsync(repeaterItems[1].isSelected()).toBeResolvedTo(false); + + await repeaterItems[1].select(); + await expectAsync(repeaterItems[1].isSelected()).toBeResolvedTo(true); + + removeButton?.click(); + fixture.detectChanges(); + + repeaterItems = await repeaterHarness.getRepeaterItems(); + expect(repeaterItems).toBeDefined(); + expect(repeaterItems.length).toBe(3); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.ts new file mode 100644 index 0000000000..7d20a4b2ba --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/example.component.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { Item } from './item'; + +let nextId = 0; + +@Component({ + selector: 'app-lists-repeater-add-remove-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + imports: [SkyDropdownModule, SkyRepeaterModule], +}) +export class ListsRepeaterAddRemoveExampleComponent { + protected items: Item[] = [ + { + title: 'Call Robert Hernandez', + note: 'Robert recently gave a very generous gift. We should call him to thank him.', + status: 'Completed', + isSelected: false, + }, + { + title: 'Send invitation to Spring Ball', + note: "The Spring Ball is coming up soon. Let's get those invitations out!", + status: 'Past due', + isSelected: false, + }, + { + title: 'Assign prospects', + note: 'There are 14 new prospects who are not assigned to fundraisers.', + status: 'Due tomorrow', + isSelected: false, + }, + { + title: 'Process gift receipts', + note: 'There are 28 recent gifts that are not receipted.', + status: 'Due next week', + isSelected: false, + }, + ]; + + protected addItem(): void { + this.items.push({ + title: 'New reminder ' + ++nextId, + note: 'This is a new reminder', + status: 'Active', + isSelected: false, + }); + } + + protected changeItems(tags: Item[]): void { + console.log('Tags in order ', tags); + } + + protected onActionClicked(buttonText: string): void { + alert(buttonText + ' was clicked!'); + } + + protected removeItems(): void { + this.items = this.items.filter((item) => !item.isSelected); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/item.ts b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/item.ts new file mode 100644 index 0000000000..a9cf29bd2a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/add-remove/item.ts @@ -0,0 +1,6 @@ +export interface Item { + title: string; + note: string; + status: string; + isSelected: boolean; +} diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.html new file mode 100644 index 0000000000..cc614285f5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.html @@ -0,0 +1,47 @@ + + @for (item of items; track item) { + + @if (item.title) { + +
+ {{ item.title }} +
+
+ {{ item.status }} +
+
+ } + + + + + + + + + + + + + + {{ item.note }} + +
+ } +
diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.scss b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.scss new file mode 100644 index 0000000000..48aa422544 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.scss @@ -0,0 +1,8 @@ +.example-repeater-flex { + display: flex; + flex-wrap: wrap; + + .example-repeater-item-title { + flex-grow: 1; + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.spec.ts new file mode 100644 index 0000000000..042424659e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.spec.ts @@ -0,0 +1,74 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyRepeaterHarness, + SkyRepeaterItemHarness, +} from '@skyux/lists/testing'; + +import { ListsRepeaterBasicExampleComponent } from './example.component'; + +describe('Repeater basic example', () => { + async function setupTest(): Promise<{ + repeaterHarness: SkyRepeaterHarness | null; + repeaterItems: SkyRepeaterItemHarness[] | null; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(ListsRepeaterBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const repeaterHarness = await loader.getHarness( + SkyRepeaterHarness.with({ dataSkyId: 'repeater-example' }), + ); + + const repeaterItems = await repeaterHarness.getRepeaterItems(); + + return { repeaterHarness, repeaterItems, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ListsRepeaterBasicExampleComponent, NoopAnimationsModule], + }); + }); + + it('should display the repeater item contents', async () => { + const { repeaterItems } = await setupTest(); + + const expectedContent = [ + { + title: 'Call Robert Hernandez Completed', + body: 'Robert recently gave a very generous gift. We should call him to thank him.', + }, + { + title: 'Send invitation to Spring Ball Past due', + body: "The Spring Ball is coming up soon. Let's get those invitations out!", + }, + { + title: 'Assign prospects Due tomorrow', + body: 'There are 14 new prospects who are not assigned to fundraisers.', + }, + { + title: 'Process gift receipts Due next week', + body: 'There are 28 recent gifts that are not receipted.', + }, + { + title: '', + body: 'Three other tasks were not displayed', + }, + ]; + + expect(repeaterItems?.length).toBe(expectedContent.length); + + if (repeaterItems) { + for (let i = 0; i < repeaterItems.length; i++) { + await expectAsync(repeaterItems[i].getTitleText()).toBeResolvedTo( + expectedContent[i].title, + ); + await expectAsync(repeaterItems[i].getContentText()).toBeResolvedTo( + expectedContent[i].body, + ); + } + } + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.ts new file mode 100644 index 0000000000..e04773b94a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/basic/example.component.ts @@ -0,0 +1,47 @@ +import { Component } from '@angular/core'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyDropdownModule } from '@skyux/popovers'; + +@Component({ + selector: 'app-lists-repeater-basic-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + imports: [SkyDropdownModule, SkyRepeaterModule], +}) +export class ListsRepeaterBasicExampleComponent { + protected items: { + note: string; + status?: string; + title?: string; + accessibilityLabel?: string; + }[] = [ + { + title: 'Call Robert Hernandez', + note: 'Robert recently gave a very generous gift. We should call him to thank him.', + status: 'Completed', + }, + { + title: 'Send invitation to Spring Ball', + note: "The Spring Ball is coming up soon. Let's get those invitations out!", + status: 'Past due', + }, + { + title: 'Assign prospects', + note: 'There are 14 new prospects who are not assigned to fundraisers.', + status: 'Due tomorrow', + }, + { + title: 'Process gift receipts', + note: 'There are 28 recent gifts that are not receipted.', + status: 'Due next week', + }, + { + note: 'Three other tasks were not displayed', + accessibilityLabel: 'Other tasks', + }, + ]; + + protected onActionClicked(buttonText: string): void { + alert(buttonText + ' was clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.html b/libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.html new file mode 100644 index 0000000000..78cae81360 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.html @@ -0,0 +1,41 @@ + + @for (item of items; track item) { + + +
+ {{ item.title }} +
+
+ + + + + + {{ item.note }} + +
+ } +
+ + +
+ + + + + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.ts new file mode 100644 index 0000000000..ae21d9d54a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/repeater/inline-form/example.component.ts @@ -0,0 +1,107 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; +import { + SkyInlineFormButtonLayout, + SkyInlineFormCloseArgs, + SkyInlineFormConfig, +} from '@skyux/inline-form'; +import { SkyRepeaterModule } from '@skyux/lists'; + +interface DemoForm { + id: FormControl; + note: FormControl; + title: FormControl; +} + +interface Item { + id: string; + title: string | undefined; + note: string | undefined; +} + +@Component({ + selector: 'app-lists-repeater-inline-form-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyIconModule, + SkyInputBoxModule, + SkyRepeaterModule, + ], +}) +export class ListsRepeaterInlineFormExampleComponent { + protected activeInlineFormId: string | undefined; + protected formGroup: FormGroup; + + protected inlineFormConfig: SkyInlineFormConfig = { + buttonLayout: SkyInlineFormButtonLayout.SaveCancel, + }; + + protected items: Item[] = [ + { + id: '1', + title: '2019 Spring Gala', + note: 'Gala for friends and family', + }, + { + id: '2', + title: '2019 Special Winter Event', + note: 'A special event', + }, + { + id: '3', + title: '2019 Donor Appreciation Event', + note: 'Event for all donors and families', + }, + { + id: '4', + title: '2020 Spring Gala', + note: 'Gala for friends and family', + }, + ]; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + id: new FormControl('', { nonNullable: true }), + title: new FormControl('', { nonNullable: true }), + note: new FormControl('', { nonNullable: true }), + }); + } + + protected showInlineForm(item: Item): void { + this.activeInlineFormId = item.id; + this.formGroup.patchValue({ + note: item.note, + title: item.title, + }); + } + + protected onInlineFormClose(args: SkyInlineFormCloseArgs): void { + if (args.reason === 'save') { + const found = this.items.find( + (item) => item.id === this.activeInlineFormId, + ); + if (found) { + found.note = this.formGroup.value.note; + found.title = this.formGroup.value.title; + } + } + + this.formGroup.patchValue({ + note: undefined, + title: undefined, + }); + + // Close the active form. + this.activeInlineFormId = undefined; + } +} diff --git a/libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.html new file mode 100644 index 0000000000..3a31a170db --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.html @@ -0,0 +1,32 @@ + + + + @for (item of sortOptions; track item) { + + {{ item.label }} + + } + + + + + @for (item of sortedItems; track item) { + + + {{ item.title }} + + +
+
Assigned to {{ item.assignee }}
+
Created {{ item.date | date }}
+
+
+ {{ item.note }} +
+
+
+ } +
diff --git a/libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.ts new file mode 100644 index 0000000000..961dbe6bdb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lists/sort/basic/example.component.ts @@ -0,0 +1,117 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkyRepeaterModule, SkySortModule } from '@skyux/lists'; + +interface Item { + title: string; + note: string; + assignee: string; + date: Date; +} +interface SortOption { + id: number; + label: string; + name: keyof Item; + descending: boolean; +} + +@Component({ + selector: 'app-lists-sort-basic-example', + templateUrl: './example.component.html', + imports: [CommonModule, SkyRepeaterModule, SkySortModule, SkyToolbarModule], +}) +export class ListsSortBasicExampleComponent implements OnInit { + protected initialState = 3; + + protected sortedItems: Item[] = [ + { + title: 'Call Robert Hernandez', + note: 'Robert recently gave a very generous gift. We should call to thank him.', + assignee: 'Debby Fowler', + date: new Date('12/22/2015'), + }, + { + title: 'Send invitation to ball', + note: "The Spring Ball is coming up soon. Let's get those invitations out!", + assignee: 'Debby Fowler', + date: new Date('1/1/2016'), + }, + { + title: 'Clean up desk', + note: 'File and organize papers.', + assignee: 'Tim Howard', + date: new Date('2/2/2016'), + }, + { + title: 'Investigate leads', + note: 'Check out leads for important charity event funding.', + assignee: 'Larry Williams', + date: new Date('4/5/2016'), + }, + { + title: 'Send thank you note', + note: 'Send a thank you note to Timothy for his donation.', + assignee: 'Catherine Hooper', + date: new Date('11/11/2015'), + }, + ]; + + protected sortOptions: SortOption[] = [ + { + id: 1, + label: 'Assigned to (A - Z)', + name: 'assignee', + descending: false, + }, + { + id: 2, + label: 'Assigned to (Z - A)', + name: 'assignee', + descending: true, + }, + { + id: 3, + label: 'Date created (newest first)', + name: 'date', + descending: true, + }, + { + id: 4, + label: 'Date created (oldest first)', + name: 'date', + descending: false, + }, + { + id: 5, + label: 'Note title (A - Z)', + name: 'title', + descending: false, + }, + { + id: 6, + label: 'Note title (Z - A)', + name: 'title', + descending: true, + }, + ]; + + public ngOnInit(): void { + this.sortItems(this.sortOptions[2]); + } + + protected sortItems(option: SortOption): void { + this.sortedItems = this.sortedItems.sort((a, b) => { + const descending = option.descending ? -1 : 1; + const sortProperty: keyof typeof a = option.name; + + if (a[sortProperty] > b[sortProperty]) { + return descending; + } else if (a[sortProperty] < b[sortProperty]) { + return -1 * descending; + } else { + return 0; + } + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.html new file mode 100644 index 0000000000..e7c9a73d84 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.html @@ -0,0 +1,44 @@ +
+
+ The following field:
+
    +
  • utilizes a custom search result template,
  • +
  • searches against the name and description properties,
  • +
  • limits the search results to two,
  • +
  • runs the search if the query is at least three characters long,
  • +
  • and fires an event when a selection is made.
  • +
+
+ +
+ + + + +
+

Form model:

+
{{ formGroup.value | json }}
+
+ + + + {{ item.name }} + +
+ {{ item.description }} +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.ts new file mode 100644 index 0000000000..64e16a0f24 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/example.component.ts @@ -0,0 +1,72 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { + SkyAutocompleteModule, + SkyAutocompleteSearchFunctionFilter, + SkyAutocompleteSelectionChange, +} from '@skyux/lookup'; + +import { Planet } from './planet'; + +@Component({ + selector: 'app-lookup-autocomplete-advanced-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyAutocompleteModule, + SkyIdModule, + ], +}) +export class LookupAutocompleteAdvancedExampleComponent { + protected farthestPlanet: FormControl; + protected formGroup: FormGroup; + + protected planets: Planet[] = [ + { + name: 'Mercury', + description: 'Mercury is a planet in our solar system.', + }, + { name: 'Venus', description: 'Venus is a planet in our solar system.' }, + { name: 'Earth', description: 'Earth is a planet in our solar system.' }, + { name: 'Mars', description: 'Mars is a planet in our solar system.' }, + { + name: 'Jupiter', + description: 'Jupiter is a planet in our solar system.', + }, + { name: 'Saturn', description: 'Saturn is a planet in our solar system.' }, + { name: 'Uranus', description: 'Uranus is a planet in our solar system.' }, + { + name: 'Neptune', + description: 'Neptune is a planet in our solar system.', + }, + ]; + + protected searchFilters: SkyAutocompleteSearchFunctionFilter[] = [ + (searchText: string, item: Planet): boolean => { + return item.name !== 'Red'; + }, + ]; + + readonly #formBuilder = inject(FormBuilder); + + constructor() { + this.farthestPlanet = this.#formBuilder.control({}); + this.formGroup = this.#formBuilder.group({ + farthestPlanet: this.farthestPlanet, + }); + } + + protected onPlanetSelection(args: SkyAutocompleteSelectionChange): void { + alert(`You selected ${(args.selectedItem as Planet).name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/planet.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/planet.ts new file mode 100644 index 0000000000..d73372fc32 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/advanced/planet.ts @@ -0,0 +1,4 @@ +export interface Planet { + name?: string; + description?: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.html new file mode 100644 index 0000000000..4b9053a955 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.html @@ -0,0 +1,18 @@ +
+
+ + + + +
+

Form model:

+
{{ formGroup.value | json }}
+
diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.spec.ts new file mode 100644 index 0000000000..11445718fd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.spec.ts @@ -0,0 +1,59 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyAutocompleteHarness } from '@skyux/lookup/testing'; + +import { LookupAutocompleteBasicExampleComponent } from './example.component'; + +describe('Basic autocomplete example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyAutocompleteHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + LookupAutocompleteBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyAutocompleteHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LookupAutocompleteBasicExampleComponent], + }); + }); + + it('should set up favorite color autocomplete input', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'favorite-color', + }); + + await harness.focus(); + await harness.enterText('b'); + + const searchResultsText = await harness.getSearchResultsText(); + + expect(searchResultsText.length).toBe(3); + + await harness.clear(); + await harness.enterText('blu'); + + const searchResults = await harness.getSearchResults(); + await expectAsync(searchResults[0].getDescriptorValue()).toBeResolvedTo( + 'Blue', + ); + await expectAsync(searchResults[0].getText()).toBeResolvedTo('Blue'); + + await searchResults[0].select(); + const value = + fixture.componentInstance.formGroup.get('favoriteColor')?.value; + expect(value?.name).toBe('Blue'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.ts new file mode 100644 index 0000000000..afbdea425f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/basic/example.component.ts @@ -0,0 +1,41 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyAutocompleteModule } from '@skyux/lookup'; + +@Component({ + selector: 'app-lookup-autocomplete-basic-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyAutocompleteModule, + SkyIdModule, + ], +}) +export class LookupAutocompleteBasicExampleComponent { + public colors: { name: string }[] = [ + { name: 'Red' }, + { name: 'Blue' }, + { name: 'Green' }, + { name: 'Orange' }, + { name: 'Pink' }, + { name: 'Purple' }, + { name: 'Yellow' }, + { name: 'Brown' }, + { name: 'Turquoise' }, + { name: 'White' }, + { name: 'Black' }, + ]; + + public formGroup = inject(FormBuilder).group({ + favoriteColor: new FormControl<{ name: string } | undefined>(undefined), + }); +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.html new file mode 100644 index 0000000000..3c0cfa8cbe --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.html @@ -0,0 +1,32 @@ +
+

+ The following field has a preselected value and utilizes a custom search + function, as well as a custom template for the search results. +

+
+ + + + +
+

Form model:

+
{{ formGroup.value | json }}
+
+ + + + {{ item.title }} • ID {{ item.id }} + diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.ts new file mode 100644 index 0000000000..bbb816f60d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/example.component.ts @@ -0,0 +1,74 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyIconModule } from '@skyux/icon'; +import { + SkyAutocompleteModule, + SkyAutocompleteSearchFunction, + SkyAutocompleteSearchFunctionResponse, +} from '@skyux/lookup'; + +import { Ocean } from './ocean'; + +@Component({ + selector: 'app-lookup-autocomplete-custom-search-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyAutocompleteModule, + SkyIconModule, + SkyIdModule, + ], +}) +export class LookupAutocompleteCustomSearchExampleComponent { + protected formGroup: FormGroup; + protected largestOcean: FormControl; + + protected oceans: Ocean[] = [ + { title: 'Arctic', id: 1 }, + { title: 'Atlantic', id: 2 }, + { title: 'Indian', id: 3 }, + { title: 'Pacific', id: 4 }, + ]; + + readonly #formBuilder = inject(FormBuilder); + + constructor() { + this.largestOcean = this.#formBuilder.control({ title: 'Arctic', id: 1 }); + this.formGroup = this.#formBuilder.group({ + largestOcean: this.largestOcean, + }); + } + + protected getOceanSearchFunction(): SkyAutocompleteSearchFunction { + const searchFunction = ( + searchText: string, + oceans: Ocean[], + ): SkyAutocompleteSearchFunctionResponse => { + return new Promise((resolve) => { + const searchTextLower = searchText.toLowerCase(); + + const results = oceans.filter((ocean: Ocean) => { + const val = ocean.title; + return !!val?.toString().toLowerCase().includes(searchTextLower); + }); + + // Simulate an async request. + setTimeout(() => { + resolve(results); + }, 500); + }); + }; + + return searchFunction; + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/ocean.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/ocean.ts new file mode 100644 index 0000000000..f6bff14514 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/custom-search/ocean.ts @@ -0,0 +1,4 @@ +export interface Ocean { + id: number; + title: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.html new file mode 100644 index 0000000000..43a4d7d7fb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.html @@ -0,0 +1,22 @@ +
+

+ The following field provides a custom function that filters the data before + every search attempt. +

+
+ + + + +
+

Form model:

+
{{ formGroup.value | json }}
+
diff --git a/libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.ts new file mode 100644 index 0000000000..10a1db2685 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/autocomplete/search-filters/example.component.ts @@ -0,0 +1,54 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { + SkyAutocompleteModule, + SkyAutocompleteSearchFunctionFilter, +} from '@skyux/lookup'; + +@Component({ + selector: 'app-lookup-autocomplete-search-filters-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyAutocompleteModule, + SkyIdModule, + ], +}) +export class LookupAutocompleteSearchFiltersExampleComponent { + protected colors: { name: string }[] = [ + { name: 'Red' }, + { name: 'Blue' }, + { name: 'Green' }, + { name: 'Orange' }, + { name: 'Pink' }, + { name: 'Purple' }, + { name: 'Yellow' }, + { name: 'Brown' }, + { name: 'Turquoise' }, + { name: 'White' }, + { name: 'Black' }, + ]; + + protected formGroup: FormGroup; + + protected searchFilters: SkyAutocompleteSearchFunctionFilter[] = [ + (searchText: string, item: { name: string }): boolean => { + return item.name !== 'Red'; + }, + ]; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + favoriteColor: undefined, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.html new file mode 100644 index 0000000000..1f26b461a3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.html @@ -0,0 +1,16 @@ +
+ + + @if (countryControl.errors?.['invalidCountry']) { + + } + +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.spec.ts new file mode 100644 index 0000000000..c5d460793f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.spec.ts @@ -0,0 +1,58 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyCountryFieldHarness } from '@skyux/lookup/testing'; + +import { LookupCountryFieldBasicExampleComponent } from './example.component'; + +describe('Basic country field example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyCountryFieldHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + LookupCountryFieldBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }), + ) + ).queryHarness(SkyCountryFieldHarness); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LookupCountryFieldBasicExampleComponent], + }); + }); + + it('should set up country field input', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: 'country-field', + }); + + await harness.focus(); + await harness.enterText('ger'); + + const searchResultsText = await harness.getSearchResultsText(); + + expect(searchResultsText.length).toBe(4); + + await harness.clear(); + await harness.enterText('can'); + + const searchResults = await harness.getSearchResults(); + await expectAsync(searchResults[1].getText()).toBeResolvedTo('Canada'); + + await searchResults[1].select(); + const value = fixture.componentInstance.countryForm.get('country')?.value; + expect(value?.name).toBe('Canada'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.ts new file mode 100644 index 0000000000..cd1e9e6738 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/country-field/basic/example.component.ts @@ -0,0 +1,60 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyCountryFieldCountry, SkyCountryFieldModule } from '@skyux/lookup'; + +interface DemoForm { + country: FormControl; +} + +function validateCountry( + control: AbstractControl, +): ValidationErrors | null { + return control.value?.name === 'Mexico' ? { invalidCountry: true } : null; +} + +@Component({ + selector: 'app-lookup-country-field-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyCountryFieldModule, + SkyInputBoxModule, + ], +}) +export class LookupCountryFieldBasicExampleComponent { + protected countryControl: FormControl; + public countryForm: FormGroup; + + protected helpPopoverContent = + 'We use the country to validate your passport within 10 business days. You can update it at any time.'; + + #formBuilder = inject(FormBuilder); + + constructor() { + this.countryControl = new FormControl( + { + name: 'Australia', + iso2: 'au', + }, + { + nonNullable: true, + validators: [validateCountry, Validators.required], + }, + ); + + this.countryForm = this.#formBuilder.group({ + country: this.countryControl, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.html new file mode 100644 index 0000000000..3e38f76abb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.html @@ -0,0 +1,23 @@ +
+ + + + + + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.ts new file mode 100644 index 0000000000..635584aaf3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/add-item-modal.component.ts @@ -0,0 +1,47 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +let nextId = 21; + +@Component({ + selector: 'app-add-item-modal', + templateUrl: './add-item-modal.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyModalModule, + ], +}) +export class AddItemModalComponent { + protected readonly formGroup: FormGroup; + + readonly #modal = inject(SkyModalInstance); + + constructor() { + this.formGroup = inject(FormBuilder).group({ + id: [`${nextId++}`], + name: ['', Validators.required], + }); + } + + protected close(): void { + this.#modal.close(); + } + + protected save(): void { + if (this.formGroup.valid) { + this.#modal.close(this.formGroup.value, 'save'); + } else { + this.formGroup.markAllAsTouched(); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.html new file mode 100644 index 0000000000..293f6c5cc2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.html @@ -0,0 +1,31 @@ +
+ + + +
+ Form model: +
{{ favoritesForm.value | json }}
+
+ +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.spec.ts new file mode 100644 index 0000000000..c7e0e5dff0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.spec.ts @@ -0,0 +1,114 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyLookupHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupAddItemExampleComponent } from './example.component'; +import { DemoService } from './example.service'; + +describe('Lookup asynchronous search example', () => { + let mockSvc!: jasmine.SpyObj; + + async function setupTest(): Promise<{ + lookupHarness: SkyLookupHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(LookupAddItemExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const lookupHarness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), + ) + ).queryHarness(SkyLookupHarness); + + return { lookupHarness, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + TestBed.configureTestingModule({ + imports: [LookupAddItemExampleComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], + }); + }); + + it('should set the expected initial value', async () => { + const { lookupHarness } = await setupTest(); + + await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ + 'Shirley', + ]); + }); + + it('should update the form control when a favorite name is selected', async () => { + const { lookupHarness, fixture } = await setupTest(); + + mockSvc.search.and.callFake((searchText) => + of({ + hasMore: false, + people: + searchText === 'b' + ? [ + { + id: '21', + name: 'Bernard', + }, + ] + : [], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('b'); + await lookupHarness.selectSearchResult({ + text: 'Bernard', + }); + + expect(fixture.componentInstance.favoritesForm.value.favoriteNames).toEqual( + [ + { id: '16', name: 'Shirley' }, + { id: '21', name: 'Bernard' }, + ], + ); + }); + + it('should respect the selection descriptor', async () => { + const { lookupHarness } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + id: '21', + name: 'Bernard', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.clickShowMoreButton(); + + const picker = await lookupHarness.getShowMorePicker(); + + await expectAsync(picker.getSearchAriaLabel()).toBeResolvedTo( + 'Search names', + ); + await expectAsync(picker.getSaveButtonAriaLabel()).toBeResolvedTo( + 'Select names', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.ts new file mode 100644 index 0000000000..b66a33390a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.component.ts @@ -0,0 +1,117 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, + SkyLookupModule, + SkyLookupShowMoreConfig, +} from '@skyux/lookup'; +import { SkyModalService } from '@skyux/modals'; + +import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { AddItemModalComponent } from './add-item-modal.component'; +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + selector: 'app-lookup-add-item-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyIdModule, + SkyInputBoxModule, + SkyLookupModule, + ], +}) +export class LookupAddItemExampleComponent implements OnInit, OnDestroy { + public favoritesForm: FormGroup<{ + favoriteNames: FormControl; + }>; + + public showMoreConfig: SkyLookupShowMoreConfig = { + nativePickerConfig: { + selectionDescriptor: 'names', + }, + }; + + #subscriptions = new Subscription(); + + readonly #svc = inject(DemoService); + readonly #modalSvc = inject(SkyModalService); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + const names = new FormControl([{ id: '16', name: 'Shirley' }]); + + this.favoritesForm = inject(FormBuilder).group({ + favoriteNames: names, + }); + } + + public ngOnInit(): void { + // If you need to execute some logic after the lookup values change, + // subscribe to Angular's built-in value changes observable. + this.favoritesForm.valueChanges.subscribe((changes) => { + console.log('Lookup value changes:', changes); + }); + } + + public ngOnDestroy(): void { + this.#subscriptions.unsubscribe(); + } + + public onSubmit(): void { + alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); + } + + public searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + public addClick(args: SkyLookupAddClickEventArgs): void { + const modal = this.#modalSvc.open(AddItemModalComponent); + + this.#subscriptions.add( + modal.closed.subscribe((close) => { + if (close.reason === 'save') { + const person = close.data as Person; + + this.#subscriptions.add( + this.#waitSvc + .blockingWrap(this.#svc.addPerson(person)) + .subscribe((data) => { + args.itemAdded({ + item: person, + data, + }); + }), + ); + } + }), + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.service.ts new file mode 100644 index 0000000000..20cd46fde3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/example.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { SearchResults } from './search-results'; + +const people: Person[] = [ + { id: '1', name: 'Abed' }, + { id: '2', name: 'Alex' }, + { id: '3', name: 'Ben' }, + { id: '4', name: 'Britta' }, + { id: '5', name: 'Buzz' }, + { id: '6', name: 'Craig' }, + { id: '7', name: 'Elroy' }, + { id: '8', name: 'Garrett' }, + { id: '9', name: 'Ian' }, + { id: '10', name: 'Jeff' }, + { id: '11', name: 'Leonard' }, + { id: '12', name: 'Neil' }, + { id: '13', name: 'Pierce' }, + { id: '14', name: 'Preston' }, + { id: '15', name: 'Rachel' }, + { id: '16', name: 'Shirley' }, + { id: '17', name: 'Todd' }, + { id: '18', name: 'Troy' }, + { id: '19', name: 'Vaughn' }, + { id: '20', name: 'Vicki' }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(people.slice()).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/person.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/person.ts new file mode 100644 index 0000000000..1ca0da72a7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/person.ts @@ -0,0 +1,4 @@ +export interface Person { + id: string; + name: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/search-results.ts new file mode 100644 index 0000000000..03d2f603c4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/add-item/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface SearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.html new file mode 100644 index 0000000000..b31a0a9ae9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.html @@ -0,0 +1,30 @@ +
+ + + +
+ Form model: +
{{ favoritesForm.value | json }}
+
+ +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.spec.ts new file mode 100644 index 0000000000..d6e85d80eb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.spec.ts @@ -0,0 +1,205 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyLookupHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupCustomPickerExampleComponent } from './example.component'; +import { DemoService } from './example.service'; +import { Person } from './person'; +import { PickerHarness } from './picker-harness'; + +const people: Person[] = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +describe('Lookup custom picker example', () => { + let mockSvc!: jasmine.SpyObj; + + async function setupTest(): Promise<{ + lookupHarness: SkyLookupHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(LookupCustomPickerExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const lookupHarness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), + ) + ).queryHarness(SkyLookupHarness); + + return { lookupHarness, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + TestBed.configureTestingModule({ + imports: [LookupCustomPickerExampleComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], + }); + }); + + it('should set the expected initial value', async () => { + const { lookupHarness } = await setupTest(); + + await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ + 'Shirley', + ]); + }); + + it('should update the form control when a favorite name is selected', async () => { + const { lookupHarness, fixture } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('Be'); + + const allResultHarnesses = await lookupHarness.getSearchResults(); + const firstResultHarness = allResultHarnesses[0]; + + if (firstResultHarness) { + await firstResultHarness.select(); + } + + expect(fixture.componentInstance.favoritesForm.value.favoriteNames).toEqual( + [ + { name: 'Shirley', formal: 'Ms. Bennett' }, + { name: 'Abed', formal: 'Mr. Nadir' }, + ], + ); + }); + + it('should use a custom picker', async () => { + const { lookupHarness, fixture } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: people, + totalCount: 20, + }), + ); + + // Show the custom picker. + await lookupHarness.clickShowMoreButton(); + + // Use the custom picker harness to validate that selecting/deselecting items + // updates the lookup form field. + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const customPickerHarness = await loader.getHarness(PickerHarness); + + await customPickerHarness.checkItemAt(2); // Ben (Mr. Chang) + await customPickerHarness.checkItemAt(7); // Garret (Mr. Lambert) + await customPickerHarness.uncheckItemAt(15); // Shirley (Ms. Bennett) + + await customPickerHarness.save(); + + expect(fixture.componentInstance.favoritesForm.value.favoriteNames).toEqual( + [ + { name: 'Ben', formal: 'Mr. Chang' }, + { name: 'Garrett', formal: 'Mr. Lambert' }, + ], + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.ts new file mode 100644 index 0000000000..73d3d87677 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.component.ts @@ -0,0 +1,124 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, + SkyLookupModule, + SkyLookupShowMoreConfig, + SkyLookupShowMoreCustomPickerContext, +} from '@skyux/lookup'; +import { SkyModalService } from '@skyux/modals'; + +import { map } from 'rxjs/operators'; + +import { DemoService } from './example.service'; +import { Person } from './person'; +import { PickerModalComponent } from './picker-modal.component'; + +@Component({ + selector: 'app-lookup-custom-picker-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyLookupModule, + ], +}) +export class LookupCustomPickerExampleComponent implements OnInit { + public favoritesForm: FormGroup<{ + favoriteNames: FormControl; + }>; + + protected showMoreConfig: SkyLookupShowMoreConfig; + + readonly #modalSvc = inject(SkyModalService); + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + const names = new FormControl([ + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + ]); + + this.favoritesForm = inject(FormBuilder).group({ + favoriteNames: names, + }); + + this.showMoreConfig = { + customPicker: { + open: (context): void => { + const instance = this.#modalSvc.open(PickerModalComponent, { + providers: [ + { + provide: SkyLookupShowMoreCustomPickerContext, + useValue: context, + }, + ], + size: 'large', + }); + + instance.closed.subscribe((closeArgs) => { + if (closeArgs.reason === 'save') { + this.favoritesForm.controls.favoriteNames.setValue( + closeArgs.data as Person[], + ); + } + }); + }, + }, + }; + } + + public ngOnInit(): void { + // If you need to execute some logic after the lookup values change, + // subscribe to Angular's built-in value changes observable. + this.favoritesForm.valueChanges.subscribe((changes) => { + console.log('Lookup value changes:', changes); + }); + } + + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + formal: 'Mr. Parker', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); + } + + protected onSubmit(): void { + alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.service.ts new file mode 100644 index 0000000000..e26d31eb38 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/example.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/person.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/person.ts new file mode 100644 index 0000000000..8dfa427af8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/person.ts @@ -0,0 +1,4 @@ +export interface Person { + name: string; + formal?: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-harness.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-harness.ts new file mode 100644 index 0000000000..9c7ca4e333 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-harness.ts @@ -0,0 +1,21 @@ +import { ComponentHarness } from '@angular/cdk/testing'; +import { SkyCheckboxHarness } from '@skyux/forms/testing'; + +export class PickerHarness extends ComponentHarness { + public static hostSelector = '.lookup-custom-picker-modal'; + + #getCheckboxes = this.locatorForAll(SkyCheckboxHarness); + #getSaveButton = this.locatorFor('.lookup-custom-picker-save-button'); + + public async checkItemAt(index: number): Promise { + await (await this.#getCheckboxes())[index].check(); + } + + public async uncheckItemAt(index: number): Promise { + await (await this.#getCheckboxes())[index].uncheck(); + } + + public async save(): Promise { + await (await this.#getSaveButton()).click(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.html new file mode 100644 index 0000000000..b444fad4e3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.html @@ -0,0 +1,39 @@ + + + @if (peopleForm) { +
+ + @for ( + personControl of peopleForm.controls.people.controls; + track personControl; + let i = $index + ) { + + + + {{ people[i].name }} + + + {{ people[i].formal }} + + + + } + +
+ } @else { +
+ +
+ } +
+ + + +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.scss b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.scss new file mode 100644 index 0000000000..deb2a87192 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.scss @@ -0,0 +1,3 @@ +.wait-container { + height: 100px; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.ts new file mode 100644 index 0000000000..5d91fd9553 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/picker-modal.component.ts @@ -0,0 +1,81 @@ +import { ChangeDetectorRef, Component, inject } from '@angular/core'; +import { + FormArray, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyCheckboxModule, SkySelectionBoxModule } from '@skyux/forms'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyWaitModule } from '@skyux/indicators'; +import { SkyLookupShowMoreCustomPickerContext } from '@skyux/lookup'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { take } from 'rxjs/operators'; + +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + selector: 'app-picker-modal', + templateUrl: './picker-modal.component.html', + styleUrls: ['./picker-modal.component.scss'], + imports: [ + FormsModule, + ReactiveFormsModule, + SkyCheckboxModule, + SkyIconModule, + SkyModalModule, + SkySelectionBoxModule, + SkyWaitModule, + ], +}) +export class PickerModalComponent { + protected peopleForm?: FormGroup<{ + people: FormArray>; + }>; + + protected people: Person[] = []; + + protected readonly context = inject(SkyLookupShowMoreCustomPickerContext); + readonly #changeDetector = inject(ChangeDetectorRef); + readonly #formBuilder = inject(FormBuilder); + readonly #modalInstance = inject(SkyModalInstance); + readonly #svc = inject(DemoService); + + constructor() { + // This list of people will be rendered as selection boxes. + this.#svc + .search('') + .pipe(take(1)) + .subscribe((results) => { + this.people = results.people; + + // Create a control for each selection box. + this.peopleForm = this.#formBuilder.group({ + people: this.#formBuilder.array( + this.people.map((item: Person) => + this.#formBuilder.control( + (this.context.initialValue as Person[]).findIndex( + (initialItem) => initialItem.name === item.name, + ) >= 0, + { nonNullable: true }, + ), + ), + ), + }); + this.#changeDetector.markForCheck(); + }); + } + + protected save(): void { + // Return a list of selected people to the lookup component. + const selectedPeople = this.people.filter( + (_, index) => this.peopleForm?.value.people?.[index], + ); + + this.#modalInstance.save(selectedPeople); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/search-results.ts new file mode 100644 index 0000000000..855f953a41 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/custom-picker/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface LookupAsyncDemoSearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.html new file mode 100644 index 0000000000..fe2c60bcee --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.html @@ -0,0 +1,37 @@ +
+ + + @if (favoritesForm.controls.favoriteNames.errors?.['letterE']) { + + } + +
+ Form model: +
{{ favoritesForm.value | json }}
+
+ +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.spec.ts new file mode 100644 index 0000000000..1e193436b3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.spec.ts @@ -0,0 +1,110 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyLookupHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupMultiSelectExampleComponent } from './example.component'; +import { DemoService } from './example.service'; + +describe('Lookup multi-select example', () => { + let mockSvc!: jasmine.SpyObj; + + async function setupTest(): Promise<{ + lookupHarness: SkyLookupHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(LookupMultiSelectExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const lookupHarness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), + ) + ).queryHarness(SkyLookupHarness); + + return { lookupHarness, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + TestBed.configureTestingModule({ + imports: [LookupMultiSelectExampleComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], + }); + }); + + it('should set the expected initial value', async () => { + const { lookupHarness } = await setupTest(); + + await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ + 'Shirley', + ]); + }); + + it('should update the form control when a favorite name is selected', async () => { + const { lookupHarness, fixture } = await setupTest(); + + mockSvc.search.and.callFake((searchText) => + of({ + hasMore: false, + people: + searchText === 'b' + ? [ + { + name: 'Bernard', + }, + ] + : [], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('b'); + await lookupHarness.selectSearchResult({ + text: 'Bernard', + }); + + expect(fixture.componentInstance.favoritesForm.value.favoriteNames).toEqual( + [{ name: 'Shirley' }, { name: 'Bernard' }], + ); + }); + + it('should respect the selection descriptor', async () => { + const { lookupHarness } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + id: '21', + name: 'Bernard', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.clickShowMoreButton(); + + const picker = await lookupHarness.getShowMorePicker(); + + await expectAsync(picker.getSearchAriaLabel()).toBeResolvedTo( + 'Search names', + ); + await expectAsync(picker.getSaveButtonAriaLabel()).toBeResolvedTo( + 'Select names', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.ts new file mode 100644 index 0000000000..5ddbdfcdc4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.component.ts @@ -0,0 +1,109 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyFormErrorModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, + SkyLookupModule, + SkyLookupShowMoreConfig, +} from '@skyux/lookup'; + +import { map } from 'rxjs/operators'; + +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + selector: 'app-lookup-multi-select-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyFormErrorModule, + SkyInputBoxModule, + SkyLookupModule, + ], +}) +export class LookupMultiSelectExampleComponent implements OnInit { + public favoritesForm: FormGroup<{ + favoriteNames: FormControl; + }>; + + public showMoreConfig: SkyLookupShowMoreConfig = { + nativePickerConfig: { + selectionDescriptor: 'names', + }, + }; + + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + const names = new FormControl([{ name: 'Shirley' }], { + validators: [ + (control: AbstractControl): ValidationErrors => { + if ( + control.value?.some((person: Person) => !person.name.match(/e/i)) + ) { + return { letterE: true }; + } + + return {}; + }, + ], + }); + + this.favoritesForm = inject(FormBuilder).group({ + favoriteNames: names, + }); + } + + public ngOnInit(): void { + // If you need to execute some logic after the lookup values change, + // subscribe to Angular's built-in value changes observable. + this.favoritesForm.valueChanges.subscribe((changes) => { + console.log('Lookup value changes:', changes); + }); + } + + protected onSubmit(): void { + alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); + } + + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.service.ts new file mode 100644 index 0000000000..e4e5bd0427 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/example.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { name: 'Abed' }, + { name: 'Alex' }, + { name: 'Ben' }, + { name: 'Britta' }, + { name: 'Buzz' }, + { name: 'Craig' }, + { name: 'Elroy' }, + { name: 'Garrett' }, + { name: 'Ian' }, + { name: 'Jeff' }, + { name: 'Leonard' }, + { name: 'Neil' }, + { name: 'Pierce' }, + { name: 'Preston' }, + { name: 'Rachel' }, + { name: 'Shirley' }, + { name: 'Todd' }, + { name: 'Troy' }, + { name: 'Vaughn' }, + { name: 'Vicki' }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/person.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/person.ts new file mode 100644 index 0000000000..3763f53ace --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/person.ts @@ -0,0 +1,3 @@ +export interface Person { + name: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/search-results.ts new file mode 100644 index 0000000000..855f953a41 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/multi-select/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface LookupAsyncDemoSearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.html new file mode 100644 index 0000000000..98ae8aa6cd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.html @@ -0,0 +1,49 @@ +
+ + + +
+ Form model: +
{{ favoritesForm.value | json }}
+
+ +
+ + + + {{ item.name }}
+ + {{ item.formal }} + +
+
+ + + {{ item.name }}
+ {{ item.formal }} +
+
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.spec.ts new file mode 100644 index 0000000000..81f5d8a40a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.spec.ts @@ -0,0 +1,174 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyLookupHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupResultTemplatesExampleComponent } from './example.component'; +import { DemoService } from './example.service'; +import { ItemHarness } from './item-harness'; + +describe('Lookup result templates example', () => { + let mockSvc!: jasmine.SpyObj; + + async function setupTest(): Promise<{ + lookupHarness: SkyLookupHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + LookupResultTemplatesExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const lookupHarness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), + ) + ).queryHarness(SkyLookupHarness); + + return { lookupHarness, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + TestBed.configureTestingModule({ + imports: [LookupResultTemplatesExampleComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], + }); + }); + + it('should set the expected initial value', async () => { + const { lookupHarness } = await setupTest(); + + await expectAsync(lookupHarness.getSelectionsText()).toBeResolvedTo([ + 'Shirley', + ]); + }); + + it('should use the expected dropdown item template', async () => { + const { lookupHarness } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('be'); + + const results = await lookupHarness.getSearchResults(); + const templateItemHarness = + results && (await results[0].queryHarness(ItemHarness)); + + await expectAsync(templateItemHarness.getName()).toBeResolvedTo('Abed'); + await expectAsync(templateItemHarness.getFormalName()).toBeResolvedTo( + 'Mr. Nadir', + ); + }); + + it('should use the expected modal item template', async () => { + const { lookupHarness } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.clickShowMoreButton(); + + const pickerHarness = await lookupHarness.getShowMorePicker(); + await pickerHarness.enterSearchText('be'); + + const results = await pickerHarness.getSearchResults(); + const templateItemHarness = + results && (await results[0].queryHarness(ItemHarness)); + + await expectAsync(templateItemHarness.getName()).toBeResolvedTo('Abed'); + await expectAsync(templateItemHarness.getFormalName()).toBeResolvedTo( + 'Mr. Nadir', + ); + }); + + it('should update the form control when a favorite name is selected', async () => { + const { lookupHarness, fixture } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('be'); + + const allResultHarnesses = await lookupHarness.getSearchResults(); + const firstResultHarness = allResultHarnesses[0]; + await firstResultHarness.select(); + + expect( + fixture.componentInstance.favoritesForm.controls.favoriteNames.value, + ).toEqual([ + { name: 'Shirley', formal: 'Ms. Bennett' }, + { name: 'Abed', formal: 'Mr. Nadir' }, + ]); + }); + + it('should respect the selection descriptor', async () => { + const { lookupHarness } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.clickShowMoreButton(); + + const picker = await lookupHarness.getShowMorePicker(); + + await expectAsync(picker.getSearchAriaLabel()).toBeResolvedTo( + 'Search names', + ); + await expectAsync(picker.getSaveButtonAriaLabel()).toBeResolvedTo( + 'Select names', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.ts new file mode 100644 index 0000000000..53eccea854 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.component.ts @@ -0,0 +1,115 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + OnInit, + TemplateRef, + ViewChild, + inject, +} from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, + SkyLookupModule, + SkyLookupShowMoreConfig, +} from '@skyux/lookup'; + +import { map } from 'rxjs/operators'; + +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + selector: 'app-lookup-result-templates-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyLookupModule, + ], +}) +export class LookupResultTemplatesExampleComponent implements OnInit { + public favoritesForm: FormGroup<{ + favoriteNames: FormControl; + }>; + + protected showMoreConfig: SkyLookupShowMoreConfig = { + nativePickerConfig: { + selectionDescriptor: 'names', + }, + }; + + @ViewChild('modalItemTemplate') + protected set modalItemTemplate(template: TemplateRef) { + if (this.showMoreConfig.nativePickerConfig) { + this.showMoreConfig.nativePickerConfig.itemTemplate = template; + } else { + this.showMoreConfig.nativePickerConfig = { itemTemplate: template }; + } + } + + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + const names = new FormControl([ + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + ]); + + this.favoritesForm = inject(FormBuilder).group({ + favoriteNames: names, + }); + } + + public ngOnInit(): void { + // If you need to execute some logic after the lookup values change, + // subscribe to Angular's built-in value changes observable. + this.favoritesForm.valueChanges.subscribe((changes) => { + console.log('Lookup value changes:', changes); + }); + } + + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + formal: 'Mr. Parker', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); + } + + protected onSubmit(): void { + alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.service.ts new file mode 100644 index 0000000000..e26d31eb38 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/example.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { + name: 'Abed', + formal: 'Mr. Nadir', + }, + { + name: 'Alex', + formal: 'Mr. Osbourne', + }, + { + name: 'Ben', + formal: 'Mr. Chang', + }, + { + name: 'Britta', + formal: 'Ms. Perry', + }, + { + name: 'Buzz', + formal: 'Mr. Hickey', + }, + { + name: 'Craig', + formal: 'Mr. Pelton', + }, + { + name: 'Elroy', + formal: 'Mr. Patashnik', + }, + { + name: 'Garrett', + formal: 'Mr. Lambert', + }, + { + name: 'Ian', + formal: 'Mr. Duncan', + }, + { + name: 'Jeff', + formal: 'Mr. Winger', + }, + { + name: 'Leonard', + formal: 'Mr. Rodriguez', + }, + { + name: 'Neil', + formal: 'Mr. Neil', + }, + { + name: 'Pierce', + formal: 'Mr. Hawthorne', + }, + { + name: 'Preston', + formal: 'Mr. Koogler', + }, + { + name: 'Rachel', + formal: 'Ms. Rachel', + }, + { + name: 'Shirley', + formal: 'Ms. Bennett', + }, + { + name: 'Todd', + formal: 'Mr. Jacobson', + }, + { + name: 'Troy', + formal: 'Mr. Barnes', + }, + { + name: 'Vaughn', + formal: 'Mr. Miller', + }, + { + name: 'Vicki', + formal: 'Ms. Jenkins', + }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/item-harness.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/item-harness.ts new file mode 100644 index 0000000000..9fdf27c561 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/item-harness.ts @@ -0,0 +1,20 @@ +import { ComponentHarness } from '@angular/cdk/testing'; + +/** + * Harness for interacting with a lookup component in tests. + * @internal + */ +export class ItemHarness extends ComponentHarness { + public static hostSelector = '.lookup-example-template-item'; + + #getName = this.locatorFor('.lookup-example-template-name'); + #getFormalName = this.locatorFor('.lookup-example-template-formal-name'); + + public async getName(): Promise { + return await (await this.#getName()).text(); + } + + public async getFormalName(): Promise { + return await (await this.#getFormalName()).text(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/person.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/person.ts new file mode 100644 index 0000000000..8dfa427af8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/person.ts @@ -0,0 +1,4 @@ +export interface Person { + name: string; + formal?: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/search-results.ts new file mode 100644 index 0000000000..855f953a41 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/result-templates/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface LookupAsyncDemoSearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.html new file mode 100644 index 0000000000..cb2be33728 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.html @@ -0,0 +1,38 @@ +
+ + + @if (favoritesForm.controls.favoriteName.errors?.['letterE']) { + + } + +
+ Form model: +
{{ favoritesForm.value | json }}
+
+ +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.spec.ts new file mode 100644 index 0000000000..48c03549ad --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.spec.ts @@ -0,0 +1,108 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyLookupHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupSingleSelectExampleComponent } from './example.component'; +import { DemoService } from './example.service'; + +describe('Lookup single-select example', () => { + let mockSvc!: jasmine.SpyObj; + + async function setupTest(): Promise<{ + lookupHarness: SkyLookupHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(LookupSingleSelectExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const lookupHarness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: 'favorite-names-field' }), + ) + ).queryHarness(SkyLookupHarness); + + return { lookupHarness, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + TestBed.configureTestingModule({ + imports: [LookupSingleSelectExampleComponent, NoopAnimationsModule], + providers: [ + { + provide: DemoService, + useValue: mockSvc, + }, + ], + }); + }); + + it('should set the expected initial value', async () => { + const { lookupHarness } = await setupTest(); + + await expectAsync(lookupHarness.getValue()).toBeResolvedTo('Shirley'); + }); + + it('should update the form control when a favorite name is selected', async () => { + const { lookupHarness, fixture } = await setupTest(); + + mockSvc.search.and.callFake((searchText) => + of({ + hasMore: false, + people: + searchText === 'b' + ? [ + { + name: 'Bernard', + }, + ] + : [], + totalCount: 1, + }), + ); + + await lookupHarness.enterText('b'); + await lookupHarness.selectSearchResult({ + text: 'Bernard', + }); + + expect(fixture.componentInstance.favoritesForm.value.favoriteName).toEqual([ + { name: 'Bernard' }, + ]); + }); + + it('should respect the selection descriptor', async () => { + const { lookupHarness } = await setupTest(); + + mockSvc.search.and.callFake(() => + of({ + hasMore: false, + people: [ + { + id: '21', + name: 'Bernard', + }, + ], + totalCount: 1, + }), + ); + + await lookupHarness.clickShowMoreButton(); + + const picker = await lookupHarness.getShowMorePicker(); + + await expectAsync(picker.getSearchAriaLabel()).toBeResolvedTo( + 'Search names', + ); + await expectAsync(picker.getSaveButtonAriaLabel()).toBeResolvedTo( + 'Select names', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.ts new file mode 100644 index 0000000000..4475a29153 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.component.ts @@ -0,0 +1,109 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; +import { SkyFormErrorModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { + SkyAutocompleteSearchAsyncArgs, + SkyLookupAddClickEventArgs, + SkyLookupModule, + SkyLookupShowMoreConfig, +} from '@skyux/lookup'; + +import { map } from 'rxjs/operators'; + +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + selector: 'app-lookup-single-select-example', + templateUrl: './example.component.html', + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SkyFormErrorModule, + SkyInputBoxModule, + SkyLookupModule, + ], +}) +export class LookupSingleSelectExampleComponent implements OnInit { + public favoritesForm: FormGroup<{ + favoriteName: FormControl; + }>; + + public showMoreConfig: SkyLookupShowMoreConfig = { + nativePickerConfig: { + selectionDescriptor: 'names', + }, + }; + + readonly #svc = inject(DemoService); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + const name = new FormControl([{ name: 'Shirley' }], { + validators: [ + (control: AbstractControl): ValidationErrors => { + if ( + control.value?.some((person: Person) => !person.name.match(/e/i)) + ) { + return { letterE: true }; + } + + return {}; + }, + ], + }); + + this.favoritesForm = inject(FormBuilder).group({ + favoriteName: name, + }); + } + + public ngOnInit(): void { + // If you need to execute some logic after the lookup values change, + // subscribe to Angular's built-in value changes observable. + this.favoritesForm.valueChanges.subscribe((changes) => { + console.log('Lookup value changes:', changes); + }); + } + + protected onSubmit(): void { + alert('Form submitted with: ' + JSON.stringify(this.favoritesForm.value)); + } + + protected searchAsync(args: SkyAutocompleteSearchAsyncArgs): void { + // In a real-world application the search service might return an Observable + // created by calling HttpClient.get(). Assigning that Observable to the result + // allows the lookup component to cancel the web request if it does not complete + // before the user searches again. + args.result = this.#svc.search(args.searchText).pipe( + map((result) => ({ + hasMore: result.hasMore, + items: result.people, + totalCount: result.totalCount, + })), + ); + } + + protected addClick(args: SkyLookupAddClickEventArgs): void { + const person: Person = { + name: 'Newman', + }; + + this.#waitSvc.blockingWrap(this.#svc.addPerson(person)).subscribe(() => { + args.itemAdded({ + item: person, + }); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.service.ts new file mode 100644 index 0000000000..e4e5bd0427 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/example.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { LookupAsyncDemoSearchResults } from './search-results'; + +const people: Person[] = [ + { name: 'Abed' }, + { name: 'Alex' }, + { name: 'Ben' }, + { name: 'Britta' }, + { name: 'Buzz' }, + { name: 'Craig' }, + { name: 'Elroy' }, + { name: 'Garrett' }, + { name: 'Ian' }, + { name: 'Jeff' }, + { name: 'Leonard' }, + { name: 'Neil' }, + { name: 'Pierce' }, + { name: 'Preston' }, + { name: 'Rachel' }, + { name: 'Shirley' }, + { name: 'Todd' }, + { name: 'Troy' }, + { name: 'Vaughn' }, + { name: 'Vicki' }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } + + public addPerson(person: Person): Observable { + // Simulate adding a person with a network call. + if (!people.some((item) => item.name === person.name)) { + people.unshift(person); + } + + return of(1).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/person.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/person.ts new file mode 100644 index 0000000000..3763f53ace --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/person.ts @@ -0,0 +1,3 @@ +export interface Person { + name: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/search-results.ts new file mode 100644 index 0000000000..855f953a41 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/lookup/single-select/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface LookupAsyncDemoSearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.html new file mode 100644 index 0000000000..1b0d8a1ee7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + @for (item of displayedItems; track item) { + + + {{ item.title }} + + +
+ {{ item.note }} +
+
+
+ } +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.spec.ts new file mode 100644 index 0000000000..39addfa232 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.spec.ts @@ -0,0 +1,56 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkySearchHarness } from '@skyux/lookup/testing'; + +import { LookupSearchBasicExampleComponent } from './example.component'; + +describe('Basic search example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkySearchHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(LookupSearchBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkySearchHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, LookupSearchBasicExampleComponent], + }); + }); + + it('should setup search component', async () => { + const { harness } = await setupTest({ + dataSkyId: 'example-search', + }); + + await expectAsync(harness.getAriaLabel()).toBeResolvedTo( + 'Search reminders', + ); + await expectAsync(harness.getPlaceholderText()).toBeResolvedTo( + 'Search through reminders.', + ); + }); + + it('should interact with search function', async () => { + const { harness } = await setupTest({ + dataSkyId: 'example-search', + }); + + await harness.enterText('Send'); + await expectAsync(harness.getValue()).toBeResolvedTo('Send'); + + await harness.clickClearButton(); + await expectAsync(harness.getValue()).toBeResolvedTo(''); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.ts new file mode 100644 index 0000000000..ffd518e462 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/search/basic/example.component.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkySearchModule } from '@skyux/lookup'; + +import { Item } from './item'; + +@Component({ + selector: 'app-lookup-search-basic-example', + templateUrl: './example.component.html', + imports: [SkyRepeaterModule, SkySearchModule, SkyToolbarModule], +}) +export class LookupSearchBasicExampleComponent { + protected displayedItems: Item[]; + + private items: Item[] = [ + { + title: 'Call Robert Hernandez', + note: 'Robert recently gave a very generous gift. We should call to thank him.', + }, + { + title: 'Send invitation to ball', + note: "The Spring Ball is coming up soon. Let's get those invitations out!", + }, + { + title: 'Clean up desk', + note: 'File and organize papers.', + }, + { + title: 'Investigate leads', + note: 'Check out leads for important charity event funding.', + }, + { + title: 'Send thank you note', + note: 'Send a thank you note to Timothy for his donation.', + }, + ]; + + protected placeholderText = 'Search through reminders.'; + protected searchAriaLabel = 'Search reminders'; + protected searchText = ''; + + constructor() { + this.displayedItems = this.items; + } + + protected searchApplied(searchText: string): void { + let filteredItems = this.items; + this.searchText = searchText; + + if (searchText) { + filteredItems = this.items.filter((item: Item) => { + let property: keyof typeof item; + + for (property in item) { + if ( + Object.prototype.hasOwnProperty.call(item, property) && + (property === 'title' || property === 'note') + ) { + if (item[property].includes(searchText)) { + return true; + } + } + } + + return false; + }); + } + + this.displayedItems = filteredItems; + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/search/basic/item.ts b/libs/components/code-examples/src/lib/modules/lookup/search/basic/item.ts new file mode 100644 index 0000000000..87806efb64 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/search/basic/item.ts @@ -0,0 +1,4 @@ +export interface Item { + title: string; + note: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.html b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.html new file mode 100644 index 0000000000..6915d928a5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.html @@ -0,0 +1,21 @@ +
+ + + + + + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.ts new file mode 100644 index 0000000000..3921c96ba7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/add-item-modal.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +let nextId = 21; + +@Component({ + selector: 'app-add-item-modal', + templateUrl: './add-item-modal.component.html', + imports: [ReactiveFormsModule, SkyInputBoxModule, SkyModalModule], +}) +export class AddItemModalComponent { + protected readonly formGroup: FormGroup; + + readonly #modal = inject(SkyModalInstance); + + constructor() { + this.formGroup = inject(FormBuilder).group({ + id: [`${nextId++}`], + name: ['', Validators.required], + }); + } + + protected close(): void { + this.#modal.close(); + } + + protected save(): void { + if (this.formGroup.valid) { + this.#modal.close(this.formGroup.value, 'save'); + } else { + this.formGroup.markAllAsTouched(); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.html new file mode 100644 index 0000000000..037b474264 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.html @@ -0,0 +1,22 @@ +
+ +
+ +@if (selectedPeople?.length) { +
+ Selected people: +
    + @for (selectedPerson of selectedPeople; track selectedPerson) { +
  • + {{ selectedPerson.name }} +
  • + } +
+
+} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.spec.ts new file mode 100644 index 0000000000..475ead5d1a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.spec.ts @@ -0,0 +1,109 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkySelectionModalHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupSelectionModalAddItemExampleComponent } from './example.component'; +import { DemoService } from './example.service'; + +describe('Selection modal example', () => { + let mockSvc: jasmine.SpyObj; + + async function setupTest(): Promise<{ + harness: SkySelectionModalHarness; + el: HTMLElement; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + LookupSelectionModalAddItemExampleComponent, + ); + const el = fixture.nativeElement as HTMLElement; + + const openBtn = el.querySelector( + '.selection-modal-example-show-btn', + ); + + openBtn?.click(); + fixture.detectChanges(); + + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const harness = await rootLoader.getHarness(SkySelectionModalHarness); + return { harness, el, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + mockSvc.search.and.callFake((searchText) => { + return of({ + hasMore: false, + people: + searchText === 'ra' + ? [ + { + id: '1', + name: 'Rachel', + }, + ] + : [], + totalCount: 1, + }); + }); + + TestBed.configureTestingModule({ + imports: [ + LookupSelectionModalAddItemExampleComponent, + NoopAnimationsModule, + ], + }); + }); + + it('should update the selected items list when an item is selected', async () => { + const { harness, el } = await setupTest(); + + await harness.enterSearchText('ra'); + await harness.selectSearchResult({ + contentText: 'Rachel', + }); + await harness.saveAndClose(); + + const selectedItemEls = el.querySelectorAll( + '.selection-modal-example-selected li', + ); + + expect(selectedItemEls).toHaveSize(1); + expect(selectedItemEls[0].innerText.trim()).toBe('Rachel'); + }); + + it('should not update the selected items list when the user cancels the selection modal', async () => { + const { harness, el } = await setupTest(); + + await harness.enterSearchText('ra'); + await harness.selectSearchResult({ + contentText: 'Rachel', + }); + await harness.cancel(); + + const selectedItemEls = el.querySelectorAll( + '.selection-modal-example-selected li', + ); + + expect(selectedItemEls).toHaveSize(0); + }); + + it('should respect the selection descriptor', async () => { + const { harness } = await setupTest(); + + await expectAsync(harness.getSearchAriaLabel()).toBeResolvedTo( + 'Search person', + ); + await expectAsync(harness.getSaveButtonAriaLabel()).toBeResolvedTo( + 'Select person', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.ts new file mode 100644 index 0000000000..787e86585e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.component.ts @@ -0,0 +1,75 @@ +import { Component, OnDestroy, inject } from '@angular/core'; +import { + SkySelectionModalAddClickEventArgs, + SkySelectionModalCloseArgs, + SkySelectionModalSearchResult, + SkySelectionModalService, +} from '@skyux/lookup'; +import { SkyModalService } from '@skyux/modals'; + +import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { AddItemModalComponent } from './add-item-modal.component'; +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + standalone: true, + selector: 'app-lookup-selection-modal-add-item-example', + templateUrl: './example.component.html', +}) +export class LookupSelectionModalAddItemExampleComponent implements OnDestroy { + protected selectedPeople: Person[] | undefined; + + #subscriptions = new Subscription(); + + readonly #modalSvc = inject(SkyModalService); + readonly #searchSvc = inject(DemoService); + readonly #selectionModalSvc = inject(SkySelectionModalService); + + public ngOnDestroy(): void { + this.#subscriptions.unsubscribe(); + } + + protected showSelectionModal(): void { + const instance = this.#selectionModalSvc.open({ + descriptorProperty: 'name', + idProperty: 'id', + selectionDescriptor: 'person', + searchAsync: (args) => + this.#searchSvc.search(args.searchText).pipe( + map( + (results): SkySelectionModalSearchResult => ({ + hasMore: results.hasMore, + items: results.people, + totalCount: results.totalCount, + }), + ), + ), + selectMode: 'single', + showAddButton: true, + addClick: (args: SkySelectionModalAddClickEventArgs) => { + const modal = this.#modalSvc.open(AddItemModalComponent); + + this.#subscriptions.add( + modal.closed.subscribe((close) => { + if (close.reason === 'save') { + const person = close.data as Person; + this.#searchSvc.addItem(person); + args.itemAdded({ item: person }); + } + }), + ); + }, + }); + + this.#subscriptions.add( + instance.closed.subscribe((args: SkySelectionModalCloseArgs) => { + if (args.reason === 'save') { + this.selectedPeople = args.selectedItems as Person[]; + } + }), + ); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.service.ts new file mode 100644 index 0000000000..caf1bf29e1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/example.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { SearchResults } from './search-results'; + +const people: Person[] = [ + { id: '1', name: 'Abed' }, + { id: '2', name: 'Alex' }, + { id: '3', name: 'Ben' }, + { id: '4', name: 'Britta' }, + { id: '5', name: 'Buzz' }, + { id: '6', name: 'Craig' }, + { id: '7', name: 'Elroy' }, + { id: '8', name: 'Garrett' }, + { id: '9', name: 'Ian' }, + { id: '10', name: 'Jeff' }, + { id: '11', name: 'Leonard' }, + { id: '12', name: 'Neil' }, + { id: '13', name: 'Pierce' }, + { id: '14', name: 'Preston' }, + { id: '15', name: 'Rachel' }, + { id: '16', name: 'Shirley' }, + { id: '17', name: 'Todd' }, + { id: '18', name: 'Troy' }, + { id: '19', name: 'Vaughn' }, + { id: '20', name: 'Vicki' }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public addItem(item: Person): void { + people.push(item); + } + + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/person.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/person.ts new file mode 100644 index 0000000000..1ca0da72a7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/person.ts @@ -0,0 +1,4 @@ +export interface Person { + id: string; + name: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/search-results.ts new file mode 100644 index 0000000000..03d2f603c4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/add-item/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface SearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.html b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.html new file mode 100644 index 0000000000..84da4c47cf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.html @@ -0,0 +1,22 @@ +
+ +
+ +@if (selectedPeople?.length) { +
+ Selected people: +
    + @for (selectedPerson of selectedPeople; track selectedPerson) { +
  • + {{ selectedPerson.name }} +
  • + } +
+
+} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.spec.ts new file mode 100644 index 0000000000..b07df24599 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.spec.ts @@ -0,0 +1,108 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkySelectionModalHarness } from '@skyux/lookup/testing'; + +import { of } from 'rxjs'; + +import { LookupSelectionModalBasicExampleComponent } from './example.component'; +import { DemoService } from './example.service'; + +describe('Selection modal example', () => { + let mockSvc: jasmine.SpyObj; + + async function setupTest(): Promise<{ + harness: SkySelectionModalHarness; + el: HTMLElement; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + LookupSelectionModalBasicExampleComponent, + ); + const el = fixture.nativeElement as HTMLElement; + const openBtn = el.querySelector( + '.selection-modal-example-show-btn', + ); + + openBtn?.click(); + fixture.detectChanges(); + + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const harness = await rootLoader.getHarness(SkySelectionModalHarness); + return { harness, el, fixture }; + } + + beforeEach(() => { + // Create a mock search service. In a real-world application, the search + // service would make a web request which should be avoided in unit tests. + mockSvc = jasmine.createSpyObj('DemoService', ['search']); + + mockSvc.search.and.callFake((searchText) => { + return of({ + hasMore: false, + people: + searchText === 'ra' + ? [ + { + id: '1', + name: 'Rachel', + }, + ] + : [], + totalCount: 1, + }); + }); + + TestBed.configureTestingModule({ + imports: [ + LookupSelectionModalBasicExampleComponent, + NoopAnimationsModule, + ], + }); + }); + + it('should update the selected items list when an item is selected', async () => { + const { harness, el } = await setupTest(); + + await harness.enterSearchText('ra'); + await harness.selectSearchResult({ + contentText: 'Rachel', + }); + await harness.saveAndClose(); + + const selectedItemEls = el.querySelectorAll( + '.selection-modal-example-selected li', + ); + + expect(selectedItemEls).toHaveSize(1); + expect(selectedItemEls[0].innerText.trim()).toBe('Rachel'); + }); + + it('should not update the selected items list when the user cancels the selection modal', async () => { + const { harness, el } = await setupTest(); + + await harness.enterSearchText('ra'); + await harness.selectSearchResult({ + contentText: 'Rachel', + }); + await harness.cancel(); + + const selectedItemEls = el.querySelectorAll( + '.selection-modal-example-selected li', + ); + + expect(selectedItemEls).toHaveSize(0); + }); + + it('should respect the selection descriptor', async () => { + const { harness } = await setupTest(); + + await expectAsync(harness.getSearchAriaLabel()).toBeResolvedTo( + 'Search person', + ); + await expectAsync(harness.getSaveButtonAriaLabel()).toBeResolvedTo( + 'Select person', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.ts new file mode 100644 index 0000000000..9cd6c3b91a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.component.ts @@ -0,0 +1,47 @@ +import { Component, inject } from '@angular/core'; +import { + SkySelectionModalSearchResult, + SkySelectionModalService, +} from '@skyux/lookup'; + +import { map } from 'rxjs/operators'; + +import { DemoService } from './example.service'; +import { Person } from './person'; + +@Component({ + standalone: true, + selector: 'app-lookup-selection-modal-basic-example', + templateUrl: './example.component.html', +}) +export class LookupSelectionModalBasicExampleComponent { + protected selectedPeople: Person[] | undefined; + + readonly #searchSvc = inject(DemoService); + readonly #selectionModalSvc = inject(SkySelectionModalService); + + protected showSelectionModal(): void { + const instance = this.#selectionModalSvc.open({ + descriptorProperty: 'name', + idProperty: 'id', + selectionDescriptor: 'person', + searchAsync: (args) => + this.#searchSvc.search(args.searchText).pipe( + map( + (results): SkySelectionModalSearchResult => ({ + hasMore: results.hasMore, + items: results.people, + totalCount: results.totalCount, + }), + ), + ), + selectMode: 'single', + }); + + instance.closed.subscribe((args) => { + if (args.reason === 'save') { + this.selectedPeople = args.selectedItems as Person[]; + } + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.service.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.service.ts new file mode 100644 index 0000000000..6518c55485 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/example.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { Person } from './person'; +import { SearchResults } from './search-results'; + +const people: Person[] = [ + { id: '1', name: 'Abed' }, + { id: '2', name: 'Alex' }, + { id: '3', name: 'Ben' }, + { id: '4', name: 'Britta' }, + { id: '5', name: 'Buzz' }, + { id: '6', name: 'Craig' }, + { id: '7', name: 'Elroy' }, + { id: '8', name: 'Garrett' }, + { id: '9', name: 'Ian' }, + { id: '10', name: 'Jeff' }, + { id: '11', name: 'Leonard' }, + { id: '12', name: 'Neil' }, + { id: '13', name: 'Pierce' }, + { id: '14', name: 'Preston' }, + { id: '15', name: 'Rachel' }, + { id: '16', name: 'Shirley' }, + { id: '17', name: 'Todd' }, + { id: '18', name: 'Troy' }, + { id: '19', name: 'Vaughn' }, + { id: '20', name: 'Vicki' }, +]; + +@Injectable({ + providedIn: 'root', +}) +export class DemoService { + public search(searchText: string): Observable { + // Simulate a network call with latency. A real-world application might + // use Angular's HttpClient to create an Observable from a call to a + // web service. + searchText = searchText.toUpperCase(); + + const matchingPeople = people.filter((person) => + person.name.toUpperCase().includes(searchText), + ); + + return of({ + hasMore: false, + people: matchingPeople, + totalCount: matchingPeople.length, + }).pipe(delay(800)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/person.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/person.ts new file mode 100644 index 0000000000..1ca0da72a7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/person.ts @@ -0,0 +1,4 @@ +export interface Person { + id: string; + name: string; +} diff --git a/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/search-results.ts b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/search-results.ts new file mode 100644 index 0000000000..03d2f603c4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/lookup/selection-modal/basic/search-results.ts @@ -0,0 +1,7 @@ +import { Person } from './person'; + +export interface SearchResults { + hasMore: boolean; + people: Person[]; + totalCount: number; +} diff --git a/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.spec.ts new file mode 100644 index 0000000000..c9b008c1a9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyConfirmTestingController, + SkyConfirmTestingModule, +} from '@skyux/modals/testing'; + +import { ModalsConfirmBasicWithControllerExampleComponent } from './example.component'; + +describe('Testing with SkyConfirmTestingController', () => { + function setupTest(): { + confirmController: SkyConfirmTestingController; + fixture: ComponentFixture; + } { + const confirmController = TestBed.inject(SkyConfirmTestingController); + const fixture = TestBed.createComponent( + ModalsConfirmBasicWithControllerExampleComponent, + ); + + return { confirmController, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + SkyConfirmTestingModule, + ModalsConfirmBasicWithControllerExampleComponent, + ], + }); + }); + + it('should click "OK" on a confirmation dialog', () => { + const { confirmController, fixture } = setupTest(); + + fixture.componentInstance.launchConfirm(); + fixture.detectChanges(); + + confirmController.expectOpen({ + message: 'Are you sure?', + }); + + confirmController.ok(); + confirmController.expectNone(); + + expect(fixture.componentInstance.selectedAction).toEqual('ok'); + }); + + it('should cancel the confirmation dialog', () => { + const { confirmController, fixture } = setupTest(); + + fixture.componentInstance.launchConfirm(); + fixture.detectChanges(); + + confirmController.expectOpen({ + message: 'Are you sure?', + }); + + confirmController.cancel(); + confirmController.expectNone(); + + expect(fixture.componentInstance.selectedAction).toEqual('cancel'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.ts b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.ts new file mode 100644 index 0000000000..9101410530 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-controller/example.component.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { SkyConfirmService } from '@skyux/modals'; + +@Component({ + selector: 'app-modals-confirm-basic-with-controller-example', + standalone: true, + template: ``, +}) +export class ModalsConfirmBasicWithControllerExampleComponent { + public selectedAction: string | undefined; + + readonly #confirmSvc = inject(SkyConfirmService); + + public launchConfirm(): void { + const dialog = this.#confirmSvc.open({ + message: 'Are you sure?', + }); + + dialog.closed.subscribe((args) => { + this.selectedAction = args.action; + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.html b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.html new file mode 100644 index 0000000000..28d4fb78bf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.html @@ -0,0 +1,49 @@ + + + + + + + + +@if (selectedAction) { + @if (selectedText) { +

+ You selected the "{{ selectedText }}" button, which has an action of "{{ + selectedAction + }}." +

+ } @else { +

+ You selected the "{{ selectedAction }}" action. +

+ } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.spec.ts new file mode 100644 index 0000000000..bb11d1f8e9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.spec.ts @@ -0,0 +1,75 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyConfirmHarness } from '@skyux/modals/testing'; + +import { ModalsConfirmBasicWithHarnessExampleComponent } from './example.component'; + +describe('Testing with SkyConfirmHarness', () => { + async function setupTest(confirmBtnClass: string): Promise<{ + confirmHarness: SkyConfirmHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + ModalsConfirmBasicWithHarnessExampleComponent, + ); + const el = fixture.nativeElement as HTMLElement; + const openBtn = el.querySelector(confirmBtnClass); + + openBtn?.click(); + + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const confirmHarness = await rootLoader.getHarness(SkyConfirmHarness); + + return { confirmHarness, fixture }; + } + + function expectDisplayedText( + fixture: ComponentFixture, + expectedText: string, + ): void { + expect( + (fixture.nativeElement as HTMLElement).querySelector( + '.displayed-text', + )?.innerText, + ).toEqual(expectedText); + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ModalsConfirmBasicWithHarnessExampleComponent], + }); + }); + + it('should show the correct text when OK is clicked on an OK confirm', async () => { + const { confirmHarness, fixture } = await setupTest('.ok-confirm-btn'); + + await confirmHarness.clickOkButton(); + + expectDisplayedText(fixture, 'You selected the "ok" action.'); + + await expectAsync(confirmHarness.getMessageText()).toBeResolvedTo( + 'Cannot delete invoice because it has vendor, credit memo, or purchase order activity.', + ); + }); + + it('should show the correct text when "Finalize" is clicked on a custom confirm', async () => { + const { confirmHarness, fixture } = await setupTest( + '.two-action-confirm-btn', + ); + + await confirmHarness.clickCustomButton({ text: 'Finalize' }); + + expectDisplayedText( + fixture, + 'You selected the "Finalize" button, which has an action of "save."', + ); + + await expectAsync(confirmHarness.getMessageText()).toBeResolvedTo( + 'Finalize report cards?', + ); + + await expectAsync(confirmHarness.getBodyText()).toBeResolvedTo( + 'Grades cannot be changed once the report cards are finalized.', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.ts b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.ts new file mode 100644 index 0000000000..6ddc5ba8aa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/confirm/basic-with-harness/example.component.ts @@ -0,0 +1,106 @@ +import { Component, inject } from '@angular/core'; +import { + SkyConfirmButtonConfig, + SkyConfirmInstance, + SkyConfirmService, + SkyConfirmType, +} from '@skyux/modals'; + +@Component({ + selector: 'app-modals-confirm-basic-with-harness-example', + standalone: true, + templateUrl: './example.component.html', +}) +export class ModalsConfirmBasicWithHarnessExampleComponent { + protected selectedAction: string | undefined; + protected selectedText: string | undefined; + + readonly #confirmSvc = inject(SkyConfirmService); + + protected openOKConfirm(): void { + const dialog: SkyConfirmInstance = this.#confirmSvc.open({ + message: + 'Cannot delete invoice because it has vendor, credit memo, or purchase order activity.', + }); + + dialog.closed.subscribe((result) => { + this.selectedText = undefined; + this.selectedAction = result.action; + }); + } + + protected openTwoActionConfirm(): void { + const buttons: SkyConfirmButtonConfig[] = [ + { text: 'Finalize', action: 'save', styleType: 'primary' }, + { text: 'Cancel', action: 'cancel', styleType: 'link' }, + ]; + + const dialog: SkyConfirmInstance = this.#confirmSvc.open({ + message: 'Finalize report cards?', + body: 'Grades cannot be changed once the report cards are finalized.', + type: SkyConfirmType.Custom, + buttons, + }); + + dialog.closed.subscribe((result) => { + this.selectedAction = result.action; + + for (const button of buttons) { + if (button.action === result.action) { + this.selectedText = button.text; + break; + } + } + }); + } + + protected openThreeActionConfirm(): void { + const buttons: SkyConfirmButtonConfig[] = [ + { text: 'Save', action: 'save', styleType: 'primary' }, + { text: 'Delete', action: 'delete' }, + { text: 'Keep working', action: 'cancel', styleType: 'link' }, + ]; + + const dialog = this.#confirmSvc.open({ + message: 'Save your changes before leaving?', + type: SkyConfirmType.Custom, + buttons, + }); + + dialog.closed.subscribe((result) => { + this.selectedAction = result.action; + + for (const button of buttons) { + if (button.action === result.action) { + this.selectedText = button.text; + break; + } + } + }); + } + + protected openDeleteConfirm(): void { + const buttons: SkyConfirmButtonConfig[] = [ + { text: 'Delete', action: 'delete', styleType: 'danger' }, + { text: 'Cancel', action: 'cancel', styleType: 'link' }, + ]; + + const dialog = this.#confirmSvc.open({ + message: 'Delete this account?', + body: 'Deleting this account may affect processes that are currently running.', + type: SkyConfirmType.Custom, + buttons, + }); + + dialog.closed.subscribe((result) => { + this.selectedAction = result.action; + + for (const button of buttons) { + if (button.action === result.action) { + this.selectedText = button.text; + break; + } + } + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.spec.ts new file mode 100644 index 0000000000..32ddd40e35 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyModalTestingController, + SkyModalTestingModule, +} from '@skyux/modals/testing'; + +import { ModalsModalBasicWithControllerExampleComponent } from './example.component'; +import { ModalComponent } from './modal.component'; + +describe('Modal example using testing controller', () => { + function setupTest(): { + fixture: ComponentFixture; + modalController: SkyModalTestingController; + } { + const fixture = TestBed.createComponent( + ModalsModalBasicWithControllerExampleComponent, + ); + const modalController = TestBed.inject(SkyModalTestingController); + + return { fixture, modalController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + SkyModalTestingModule, + ModalsModalBasicWithControllerExampleComponent, + ], + }); + }); + + it('should expect a modal to be open, close it, and expect none', () => { + const { fixture, modalController } = setupTest(); + + fixture.componentInstance.openModal(); + fixture.detectChanges(); + + modalController.expectCount(1); + modalController.expectOpen(ModalComponent); + modalController.closeTopModal({ + data: {}, + reason: 'save', + }); + modalController.expectNone(); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.ts new file mode 100644 index 0000000000..3c885790d0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/example.component.ts @@ -0,0 +1,71 @@ +import { Component, OnDestroy, inject } from '@angular/core'; +import { SkyHelpService } from '@skyux/core'; +import { + SkyModalError, + SkyModalInstance, + SkyModalService, +} from '@skyux/modals'; + +import { MyHelpService } from './help.service'; +import { ModalContext } from './modal-context'; +import { ModalComponent } from './modal.component'; + +@Component({ + selector: 'app-modals-modal-basic-with-controller-example', + standalone: true, + template: ``, +}) +export class ModalsModalBasicWithControllerExampleComponent + implements OnDestroy +{ + public hasErrors = false; + + protected errors: SkyModalError[] = []; + + readonly #instances: SkyModalInstance[] = []; + readonly #modalSvc = inject(SkyModalService); + + public ngOnDestroy(): void { + this.#instances.forEach((i) => { + i.close(); + }); + } + + public openModal(): void { + const instance = this.#modalSvc.open(ModalComponent, { + providers: [ + { + provide: ModalContext, + useValue: { value1: 'Hello!' }, + }, + // NOTE: The help service is normally provided at the application root, but + // it is added here purely for examplenstration purposes. + // See: https://developer.blackbaud.com/skyux/learn/develop/global-help + { + provide: SkyHelpService, + useExisting: MyHelpService, + }, + ], + }); + + instance.beforeClose.subscribe((handler) => { + if (this.hasErrors && handler.closeArgs.reason !== 'cancel') { + this.errors = [ + { + message: 'Something bad happened.', + }, + ]; + } else { + handler.closeModal(); + } + }); + + this.#instances.push(instance); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/help.service.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/help.service.ts new file mode 100644 index 0000000000..4375c64d1c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/help.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { + SkyHelpOpenArgs, + SkyHelpService, + SkyHelpUpdateArgs, +} from '@skyux/core'; + +/** + * This is a mock implementation of the help service. In a production scenario, + * the `SkyHelpService` would be provided at the application root. + * @see: https://developer.blackbaud.com/skyux/learn/develop/global-help + */ +@Injectable({ providedIn: 'root' }) +export class MyHelpService extends SkyHelpService { + public override openHelp(args?: SkyHelpOpenArgs): void { + if (args) { + console.error(`Open help panel to key: ${args.helpKey}`); + } + } + + public override updateHelp(args: SkyHelpUpdateArgs): void { + if ('helpKey' in args) { + console.error(`help key update: ${args.helpKey}`); + } + + if ('pageDefaultHelpKey' in args) { + console.error(`page default help key update: ${args.pageDefaultHelpKey}`); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal-context.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal-context.ts new file mode 100644 index 0000000000..17fd7f164c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal-context.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class ModalContext { + public data: + | { + value1: string; + } + | undefined; +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal.component.ts new file mode 100644 index 0000000000..84728da974 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-controller/modal.component.ts @@ -0,0 +1,64 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { ModalContext } from './modal-context'; + +@Component({ + imports: [ + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyModalModule, + ], + template: ` +
+ + + + + + + + + + + +
+ `, +}) +export class ModalComponent { + protected exampleForm: FormGroup<{ + value1: FormControl; + }>; + + readonly #context = inject(ModalContext); + readonly #instance = inject(SkyModalInstance); + + constructor() { + this.exampleForm = inject(FormBuilder).group({ + value1: new FormControl(this.#context.data?.value1), + }); + } + + protected cancelForm(): void { + this.#instance.cancel(); + } + + protected saveForm(): void { + this.#instance.save({}); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/context.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/context.ts new file mode 100644 index 0000000000..c2fa53d55a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/context.ts @@ -0,0 +1,5 @@ +import { ModalDemoData } from './data'; + +export class ModalDemoContext { + constructor(public data: ModalDemoData) {} +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.service.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.service.ts new file mode 100644 index 0000000000..208e7535a9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { ModalDemoData } from './data'; + +@Injectable({ + providedIn: 'root', +}) +export class ModalDemoDataService { + #data: ModalDemoData = { + value1: 'Hello world', + }; + + public load(): Observable { + // Simulate a network request to get data. + return of(this.#data).pipe(delay(1000)); + } + + public save(data: ModalDemoData): Observable { + this.#data = data; + + // Simulate a network request to save data. + return of(this.#data).pipe(delay(1000)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.ts new file mode 100644 index 0000000000..efe533e1b5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/data.ts @@ -0,0 +1,3 @@ +export interface ModalDemoData { + value1?: string | null; +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.html b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.html new file mode 100644 index 0000000000..7e220e5254 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.html @@ -0,0 +1,10 @@ + +@if (exampleValue) { +
The user entered {{ exampleValue }}
+} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.spec.ts new file mode 100644 index 0000000000..b200e8a3f2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.spec.ts @@ -0,0 +1,69 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalHarness } from '@skyux/modals/testing'; + +import { Observable, of } from 'rxjs'; + +import { ModalDemoDataService } from './data.service'; +import { ModalsModalBasicWithHarnessHelpKeyExampleComponent } from './example.component'; + +class mockWaitSvc { + public blockingWrap(data: unknown): Observable { + return of(data); + } +} + +describe('Basic modal', () => { + async function setupTest(): Promise<{ + modalHarness: SkyModalHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + ModalsModalBasicWithHarnessHelpKeyExampleComponent, + ); + fixture.componentInstance.onOpenModalClick(); + fixture.detectChanges(); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const modalHarness = await loader.getHarness( + SkyModalHarness.with({ + dataSkyId: 'modal-example', + }), + ); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { modalHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ModalsModalBasicWithHarnessHelpKeyExampleComponent, + NoopAnimationsModule, + SkyHelpTestingModule, + ], + providers: [ + { + provide: SkyWaitService, + useClass: mockWaitSvc, + }, + ModalDemoDataService, + ], + }); + }); + + it('should have the correct help key', async () => { + const { modalHarness, helpController } = await setupTest(); + + await modalHarness.clickHelpInline(); + + helpController.expectCurrentHelpKey('modal-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.ts new file mode 100644 index 0000000000..5f13156170 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/example.component.ts @@ -0,0 +1,62 @@ +import { Component, OnDestroy, inject } from '@angular/core'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { ModalDemoContext } from './context'; +import { ModalDemoData } from './data'; +import { ModalDemoDataService } from './data.service'; +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-modals-modal-basic-with-harness-help-key-example', + templateUrl: './example.component.html', +}) +export class ModalsModalBasicWithHarnessHelpKeyExampleComponent + implements OnDestroy +{ + protected modalSize = 'medium'; + protected exampleValue: string | null | undefined; + + #ngUnsubscribe = new Subject(); + + readonly #dataSvc = inject(ModalDemoDataService); + readonly #modalSvc = inject(SkyModalService); + readonly #waitSvc = inject(SkyWaitService); + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + public onOpenModalClick(): void { + // Display a blocking wait while data is loaded from the data service. + this.#waitSvc + .blockingWrap(this.#dataSvc.load()) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((data) => { + const options: SkyModalConfigurationInterface = { + providers: [ + { + provide: ModalDemoContext, + useValue: new ModalDemoContext(data), + }, + ], + size: this.modalSize, + }; + + // Show the modal after data is loaded. + const instance = this.#modalSvc.open(ModalComponent, options); + + instance.closed.subscribe((result) => { + if (result.reason === 'save') { + // Display the updated value. + this.exampleValue = (result.data as ModalDemoData).value1; + } + }); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.html b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.html new file mode 100644 index 0000000000..3f3447b9fb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.html @@ -0,0 +1,20 @@ +
+ + + + + + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.ts new file mode 100644 index 0000000000..f0df0ad676 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness-help-key/modal.component.ts @@ -0,0 +1,45 @@ +import { Component, inject } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { ModalDemoContext } from './context'; +import { ModalDemoDataService } from './data.service'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [ReactiveFormsModule, SkyInputBoxModule, SkyModalModule], +}) +export class ModalComponent { + protected exampleForm: FormGroup<{ + value1: FormControl; + }>; + + readonly #context = inject(ModalDemoContext); + readonly #dataSvc = inject(ModalDemoDataService); + readonly #instance = inject(SkyModalInstance); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + this.exampleForm = new FormGroup({ + value1: new FormControl(this.#context.data.value1), + }); + } + + protected saveForm(): void { + // Use the data service to save the data. + + this.#waitSvc + .blockingWrap(this.#dataSvc.save(this.exampleForm.value)) + .subscribe((data) => { + // Notify the modal instance that data was saved and return the saved data. + this.#instance.save(data); + }); + } + + protected cancelForm(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/context.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/context.ts new file mode 100644 index 0000000000..c2fa53d55a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/context.ts @@ -0,0 +1,5 @@ +import { ModalDemoData } from './data'; + +export class ModalDemoContext { + constructor(public data: ModalDemoData) {} +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.service.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.service.ts new file mode 100644 index 0000000000..208e7535a9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { ModalDemoData } from './data'; + +@Injectable({ + providedIn: 'root', +}) +export class ModalDemoDataService { + #data: ModalDemoData = { + value1: 'Hello world', + }; + + public load(): Observable { + // Simulate a network request to get data. + return of(this.#data).pipe(delay(1000)); + } + + public save(data: ModalDemoData): Observable { + this.#data = data; + + // Simulate a network request to save data. + return of(this.#data).pipe(delay(1000)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.ts new file mode 100644 index 0000000000..efe533e1b5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/data.ts @@ -0,0 +1,3 @@ +export interface ModalDemoData { + value1?: string | null; +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.html b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.html new file mode 100644 index 0000000000..7e220e5254 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.html @@ -0,0 +1,10 @@ + +@if (exampleValue) { +
The user entered {{ exampleValue }}
+} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.spec.ts new file mode 100644 index 0000000000..4ad8455818 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.spec.ts @@ -0,0 +1,73 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalHarness } from '@skyux/modals/testing'; + +import { Observable, of } from 'rxjs'; + +import { ModalDemoDataService } from './data.service'; +import { ModalsModalBasicWithHarnessExampleComponent } from './example.component'; + +class mockWaitSvc { + public blockingWrap(data: unknown): Observable { + return of(data); + } +} + +describe('Basic modal', () => { + async function setupTest(): Promise<{ + modalHarness: SkyModalHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + ModalsModalBasicWithHarnessExampleComponent, + ); + fixture.componentInstance.onOpenModalClick(); + fixture.detectChanges(); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const modalHarness = await loader.getHarness( + SkyModalHarness.with({ + dataSkyId: 'modal-example', + }), + ); + + return { modalHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ModalsModalBasicWithHarnessExampleComponent, + NoopAnimationsModule, + ], + providers: [ + { + provide: SkyWaitService, + useClass: mockWaitSvc, + }, + ModalDemoDataService, + ], + }); + }); + + it('should open the correct modal', async () => { + const { modalHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(modalHarness.getAriaRole()).toBeResolvedTo('dialog'); + await expectAsync(modalHarness.getSize()).toBeResolvedTo('medium'); + await expectAsync(modalHarness.isFullPage()).toBeResolvedTo(false); + await expectAsync(modalHarness.getHeadingText()).toBeResolvedTo( + 'Modal title', + ); + + await modalHarness.clickHelpInline(); + + await expectAsync(modalHarness.getHelpPopoverContent()).toBeResolvedTo( + 'Use the help inline component to invoke contextual user assistance.', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.ts new file mode 100644 index 0000000000..c805d94ec1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/example.component.ts @@ -0,0 +1,60 @@ +import { Component, OnDestroy, inject } from '@angular/core'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { ModalDemoContext } from './context'; +import { ModalDemoData } from './data'; +import { ModalDemoDataService } from './data.service'; +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-modals-modal-basic-with-harness-example', + templateUrl: './example.component.html', +}) +export class ModalsModalBasicWithHarnessExampleComponent implements OnDestroy { + protected modalSize = 'medium'; + protected exampleValue: string | null | undefined; + + #ngUnsubscribe = new Subject(); + + readonly #dataSvc = inject(ModalDemoDataService); + readonly #modalSvc = inject(SkyModalService); + readonly #waitSvc = inject(SkyWaitService); + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + public onOpenModalClick(): void { + // Display a blocking wait while data is loaded from the data service. + this.#waitSvc + .blockingWrap(this.#dataSvc.load()) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((data) => { + const options: SkyModalConfigurationInterface = { + providers: [ + { + provide: ModalDemoContext, + useValue: new ModalDemoContext(data), + }, + ], + size: this.modalSize, + }; + + // Show the modal after data is loaded. + const instance = this.#modalSvc.open(ModalComponent, options); + + instance.closed.subscribe((result) => { + if (result.reason === 'save') { + // Display the updated value. + this.exampleValue = (result.data as ModalDemoData).value1; + } + }); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.html b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.html new file mode 100644 index 0000000000..b00e306799 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.html @@ -0,0 +1,20 @@ +
+ + + + + + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.ts new file mode 100644 index 0000000000..f0df0ad676 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/basic-with-harness/modal.component.ts @@ -0,0 +1,45 @@ +import { Component, inject } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { ModalDemoContext } from './context'; +import { ModalDemoDataService } from './data.service'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [ReactiveFormsModule, SkyInputBoxModule, SkyModalModule], +}) +export class ModalComponent { + protected exampleForm: FormGroup<{ + value1: FormControl; + }>; + + readonly #context = inject(ModalDemoContext); + readonly #dataSvc = inject(ModalDemoDataService); + readonly #instance = inject(SkyModalInstance); + readonly #waitSvc = inject(SkyWaitService); + + constructor() { + this.exampleForm = new FormGroup({ + value1: new FormControl(this.#context.data.value1), + }); + } + + protected saveForm(): void { + // Use the data service to save the data. + + this.#waitSvc + .blockingWrap(this.#dataSvc.save(this.exampleForm.value)) + .subscribe((data) => { + // Notify the modal instance that data was saved and return the saved data. + this.#instance.save(data); + }); + } + + protected cancelForm(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/context.ts b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/context.ts new file mode 100644 index 0000000000..c2fa53d55a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/context.ts @@ -0,0 +1,5 @@ +import { ModalDemoData } from './data'; + +export class ModalDemoContext { + constructor(public data: ModalDemoData) {} +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.service.ts b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.service.ts new file mode 100644 index 0000000000..61e923736c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; + +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { ModalDemoData } from './data'; + +@Injectable({ + providedIn: 'root', +}) +export class ModalDemoDataService { + #data: ModalDemoData = { + value1: 'Hello world', + }; + + public load(): Observable { + // Simulate a network request to get data. + return of(this.#data).pipe(delay(1000)); + } + + public save(data: ModalDemoData, error = false): Observable { + if (!error) { + this.#data = data; + } + + // Simulate a network request to save data. + return of(this.#data).pipe(delay(1000)); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.ts b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.ts new file mode 100644 index 0000000000..efe533e1b5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/data.ts @@ -0,0 +1,3 @@ +export interface ModalDemoData { + value1?: string | null; +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.html b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.html new file mode 100644 index 0000000000..7e220e5254 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.html @@ -0,0 +1,10 @@ + +@if (exampleValue) { +
The user entered {{ exampleValue }}
+} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.spec.ts new file mode 100644 index 0000000000..a4d6727038 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.spec.ts @@ -0,0 +1,60 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalHarness } from '@skyux/modals/testing'; + +import { Observable, of } from 'rxjs'; + +import { ModalDemoDataService } from './data.service'; +import { ModalsModalWithErrorExampleComponent } from './example.component'; + +class mockWaitSvc { + public blockingWrap(data: unknown): Observable { + return of(data); + } +} + +describe('Basic modal', () => { + async function setupTest(): Promise<{ + modalHarness: SkyModalHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + ModalsModalWithErrorExampleComponent, + ); + fixture.componentInstance.onOpenModalClick(); + fixture.detectChanges(); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const modalHarness = await loader.getHarness( + SkyModalHarness.with({ + dataSkyId: 'modal-example', + }), + ); + + return { modalHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ModalsModalWithErrorExampleComponent], + providers: [ + { + provide: SkyWaitService, + useClass: mockWaitSvc, + }, + ModalDemoDataService, + ], + }); + }); + + it('should open the correct modal', async () => { + const { modalHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(modalHarness.getAriaRole()).toBeResolvedTo('dialog'); + await expectAsync(modalHarness.getSize()).toBeResolvedTo('medium'); + await expectAsync(modalHarness.isFullPage()).toBeResolvedTo(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.ts new file mode 100644 index 0000000000..c61b3fdc47 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/example.component.ts @@ -0,0 +1,60 @@ +import { Component, OnDestroy, inject } from '@angular/core'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalConfigurationInterface, SkyModalService } from '@skyux/modals'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { ModalDemoContext } from './context'; +import { ModalDemoData } from './data'; +import { ModalDemoDataService } from './data.service'; +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-modals-modal-with-error-example', + templateUrl: './example.component.html', +}) +export class ModalsModalWithErrorExampleComponent implements OnDestroy { + protected modalSize = 'medium'; + protected exampleValue: string | null | undefined; + + #ngUnsubscribe = new Subject(); + + readonly #dataSvc = inject(ModalDemoDataService); + readonly #modalSvc = inject(SkyModalService); + readonly #waitSvc = inject(SkyWaitService); + + public ngOnDestroy(): void { + this.#ngUnsubscribe.next(); + this.#ngUnsubscribe.complete(); + } + + public onOpenModalClick(): void { + // Display a blocking wait while data is loaded from the data service. + this.#waitSvc + .blockingWrap(this.#dataSvc.load()) + .pipe(takeUntil(this.#ngUnsubscribe)) + .subscribe((data) => { + const options: SkyModalConfigurationInterface = { + providers: [ + { + provide: ModalDemoContext, + useValue: new ModalDemoContext(data), + }, + ], + size: this.modalSize, + }; + + // Show the modal after data is loaded. + const instance = this.#modalSvc.open(ModalComponent, options); + + instance.closed.subscribe((result) => { + if (result.reason === 'save') { + // Display the updated value. + this.exampleValue = (result.data as ModalDemoData).value1; + } + }); + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.html b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.html new file mode 100644 index 0000000000..ad77838bde --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.html @@ -0,0 +1,27 @@ +
+ + + + + + + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.ts b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.ts new file mode 100644 index 0000000000..8048a4c9d4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/modals/modal/with-error/modal.component.ts @@ -0,0 +1,57 @@ +import { Component, inject } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyWaitService } from '@skyux/indicators'; +import { SkyModalError, SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { ModalDemoContext } from './context'; +import { ModalDemoDataService } from './data.service'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [ReactiveFormsModule, SkyInputBoxModule, SkyModalModule], +}) +export class ModalComponent { + protected exampleForm: FormGroup<{ + value1: FormControl; + }>; + + readonly #context = inject(ModalDemoContext); + readonly #dataSvc = inject(ModalDemoDataService); + readonly #instance = inject(SkyModalInstance); + readonly #waitSvc = inject(SkyWaitService); + + public errors: SkyModalError[] = []; + + constructor() { + this.exampleForm = new FormGroup({ + value1: new FormControl(this.#context.data.value1), + }); + } + + protected saveForm(): void { + // Use the data service to save the data. + + this.#waitSvc + .blockingWrap(this.#dataSvc.save(this.exampleForm.value)) + .subscribe((data) => { + // Notify the modal instance that data was saved and return the saved data. + this.#instance.save(data); + }); + } + + protected saveFormWithFormError(): void { + // Use the data service to save the data. + + this.#waitSvc + .blockingWrap(this.#dataSvc.save(this.exampleForm.value, true)) + .subscribe(() => { + this.errors = [{ message: 'There was an error saving the form.' }]; + }); + } + + protected cancelForm(): void { + this.#instance.cancel(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.html b/libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.html new file mode 100644 index 0000000000..cd6ee0edfa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.html @@ -0,0 +1,23 @@ + + + + Selected item + + + + + Show dropdown + + + Item 1 + + + Item 2 + + + Item 3 + + + + + diff --git a/libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.ts b/libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.ts new file mode 100644 index 0000000000..a57f939d5c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/navbar/navbar/example.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SkyNavbarModule } from '@skyux/navbar'; +import { SkyDropdownModule } from '@skyux/popovers'; + +@Component({ + selector: 'app-navbar-example', + templateUrl: './example.component.html', + imports: [RouterModule, SkyDropdownModule, SkyNavbarModule], +}) +export class NavbarExampleComponent { + protected onDropdownItemClick(buttonText: string): void { + alert(buttonText + ' button clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.html b/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.html new file mode 100644 index 0000000000..127e29a74a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.html @@ -0,0 +1,25 @@ + + + @for (button of buttons; track button) { + + {{ button.label }} + + } + + + +

Additional content

+

More content can go here as needed for the functional area.

+
+
diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.spec.ts new file mode 100644 index 0000000000..e4401ac92c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.spec.ts @@ -0,0 +1,110 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ApplicationRef } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + SkyModalTestingController, + SkyModalTestingModule, +} from '@skyux/modals/testing'; +import { SkyActionHubHarness } from '@skyux/pages/testing'; + +import { PagesActionHubExampleComponent } from './example.component'; +import { SettingsModalComponent } from './settings-modal.component'; + +describe('Action hub', () => { + async function setupTest(): Promise<{ + actionHubHarness: SkyActionHubHarness; + fixture: ComponentFixture; + loader: HarnessLoader; + }> { + const fixture = TestBed.createComponent(PagesActionHubExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const actionHubHarness = await loader.getHarness( + SkyActionHubHarness.with({ + dataSkyId: 'action-hub', + }), + ); + + return { actionHubHarness, fixture, loader }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PagesActionHubExampleComponent, SkyModalTestingModule], + }); + }); + + it('should have correct page title', async () => { + const { actionHubHarness } = await setupTest(); + + expect(await actionHubHarness.getTitle()).toBe('Active accounts'); + }); + + it('should show needs-attention items', async () => { + const { actionHubHarness } = await setupTest(); + + expect( + await actionHubHarness + .getNeedsAttentionItems() + .then( + async (items) => + await Promise.all(items.map((item) => item.getText())), + ), + ).toEqual([ + '9 updates from portal', + '8 new messages from online donation', + '7 possible duplicates from constituent lists', + '6 updates from portal', + '5 new messages from online donation', + '4 possible duplicates from constituent lists', + '3 update from portal', + '2 new messages from online donation', + '1 possible duplicate from constituent lists', + ]); + }); + + it('should show recent links', async () => { + const { actionHubHarness } = await setupTest(); + + const linkListHarness = await actionHubHarness.getRecentLinks(); + const listItems = await linkListHarness.getListItems(); + expect(await Promise.all(listItems.map((item) => item.getText()))).toEqual([ + 'Recent 1', + 'Recent 2', + 'Recent 3', + 'Recent 4', + 'Recent 5', + ]); + }); + + it('should show related links', async () => { + const { actionHubHarness } = await setupTest(); + + const linkListHarness = await actionHubHarness.getRelatedLinks(); + const listItems = await linkListHarness.getListItems(); + expect(await Promise.all(listItems.map((item) => item.getText()))).toEqual([ + 'Link 1', + 'Link 2', + 'Link 3', + ]); + }); + + it('should show settings links', async () => { + const { actionHubHarness } = await setupTest(); + + const linkListHarness = await actionHubHarness.getSettingsLinks(); + const listItems = await linkListHarness.getListItems(); + expect(await Promise.all(listItems.map((item) => item.getText()))).toEqual([ + 'Number', + 'Color', + ]); + const modalController = TestBed.inject(SkyModalTestingController); + modalController.expectNone(); + await listItems[0].click(); + const app = TestBed.inject(ApplicationRef); + app.tick(); + await app.whenStable(); + modalController.expectCount(1); + modalController.expectOpen(SettingsModalComponent); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.ts new file mode 100644 index 0000000000..da67675830 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/example.component.ts @@ -0,0 +1,202 @@ +import { Component } from '@angular/core'; +import { SkyActionHubModule, SkyPageModalLinksInput } from '@skyux/pages'; + +import { MODAL_TITLE } from './modal-title-token'; +import { SettingsModalComponent } from './settings-modal.component'; + +const pastHours = Array.from(Array(5).keys()).map((i) => { + const date = new Date(); + date.setHours(date.getHours() - (i + 1)); + return date; +}); + +@Component({ + selector: 'app-pages-action-hub-example', + templateUrl: './example.component.html', + imports: [SkyActionHubModule], +}) +export class PagesActionHubExampleComponent { + public buttons = [ + { + label: 'Action 1', + ariaLabel: 'Accounts action 1', + permalink: { + url: '#', + }, + }, + { + label: 'Action 2', + ariaLabel: 'Accounts action 2', + permalink: { + url: '#', + }, + }, + { + label: 'Action 3', + ariaLabel: 'Accounts action 3', + permalink: { + url: '#', + }, + }, + ]; + + public needsAttention = [ + { + title: '9', + message: 'updates from portal', + permalink: { + url: '#', + }, + }, + { + title: '8', + message: 'new messages from online donation', + permalink: { + url: '#', + }, + }, + { + title: '7', + message: 'possible duplicates from constituent lists', + permalink: { + url: '#', + }, + }, + { + title: '6', + message: 'updates from portal', + permalink: { + url: '#', + }, + }, + { + title: '5', + message: 'new messages from online donation', + permalink: { + url: '#', + }, + }, + { + title: '4', + message: 'possible duplicates from constituent lists', + permalink: { + url: '#', + }, + }, + { + title: '3', + message: 'update from portal', + permalink: { + url: '#', + }, + }, + { + title: '2', + message: 'new messages from online donation', + permalink: { + url: '#', + }, + }, + { + title: '1', + message: 'possible duplicate from constituent lists', + permalink: { + url: '#', + }, + }, + ]; + + public recentLinks = [ + { + label: 'Recent 1', + permalink: { + url: '#', + }, + lastAccessed: pastHours[0], + }, + { + label: 'Recent 2', + permalink: { + url: '#', + }, + lastAccessed: pastHours[1], + }, + { + label: 'Recent 3', + permalink: { + url: '#', + }, + lastAccessed: pastHours[2], + }, + { + label: 'Recent 4', + permalink: { + url: '#', + }, + lastAccessed: pastHours[3], + }, + { + label: 'Recent 5', + permalink: { + url: '#', + }, + lastAccessed: pastHours[4], + }, + ]; + + public relatedLinks = [ + { + label: 'Link 1', + permalink: { + url: '#', + }, + }, + { + label: 'Link 2', + permalink: { + url: '#', + }, + }, + { + label: 'Link 3', + permalink: { + url: '#', + }, + }, + ]; + + public settingsLinks: SkyPageModalLinksInput = [ + { + label: 'Number', + modal: { + component: SettingsModalComponent, + config: { + size: 'large', + providers: [ + { + provide: MODAL_TITLE, + useValue: 'Number', + }, + ], + }, + }, + }, + { + label: 'Color', + modal: { + component: SettingsModalComponent, + config: { + size: 'large', + providers: [ + { + provide: MODAL_TITLE, + useValue: 'Color', + }, + ], + }, + }, + }, + ]; + + public title = 'Active accounts'; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/modal-title-token.ts b/libs/components/code-examples/src/lib/modules/pages/action-hub/modal-title-token.ts new file mode 100644 index 0000000000..9e20862043 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/modal-title-token.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const MODAL_TITLE = new InjectionToken('MODAL_TITLE'); diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.html b/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.html new file mode 100644 index 0000000000..55469c6d8b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.html @@ -0,0 +1,41 @@ +
+ + + @for (field of fields; track field) { + + @switch (title) { + @case ('Color') { + + + + } + @default { + + + + } + } + + } + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.spec.ts new file mode 100644 index 0000000000..be5033d948 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.spec.ts @@ -0,0 +1,70 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyModalHarness } from '@skyux/modals/testing'; +import { SkyActionHubHarness } from '@skyux/pages/testing'; + +import { PagesActionHubExampleComponent } from './example.component'; +import { MODAL_TITLE } from './modal-title-token'; + +describe('SettingsModalComponent', () => { + async function setupTest(): Promise<{ + actionHubHarness: SkyActionHubHarness; + fixture: ComponentFixture; + loader: HarnessLoader; + rootLoader: HarnessLoader; + }> { + const fixture = TestBed.createComponent(PagesActionHubExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + const rootLoader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const actionHubHarness = await loader.getHarness( + SkyActionHubHarness.with({ + dataSkyId: 'action-hub', + }), + ); + + return { actionHubHarness, rootLoader, fixture, loader }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PagesActionHubExampleComponent, NoopAnimationsModule], + providers: [ + { + provide: MODAL_TITLE, + useValue: 'Settings Modal Test', + }, + ], + }); + }); + + it('should show settings modal', async () => { + const { actionHubHarness, rootLoader } = await setupTest(); + const settingsLinksHarness = await actionHubHarness.getSettingsLinks(); + const settingsLinks = await settingsLinksHarness.getListItems(); + expect(settingsLinks).toHaveSize(2); + await settingsLinks[1].click(); + const modalHarness = await rootLoader.getHarness( + SkyModalHarness.with({ + dataSkyId: 'settings-modal', + }), + ); + expect(await modalHarness.getHeadingText()).toBe('Color'); + const consoleSpy = spyOn(console, 'log').and.stub(); + const modalElement = TestbedHarnessEnvironment.getNativeElement( + await modalHarness.host(), + ); + const submit = modalElement.querySelector('button[type="submit"]'); + expect(submit).toBeTruthy(); + expect(submit?.textContent?.trim()).toBe('Save'); + (submit as HTMLButtonElement).click(); + expect(consoleSpy).toHaveBeenCalledWith({ + 'Color 1': '', + 'Color 2': '', + 'Color 3': '', + 'Color 4': '', + 'Color 5': '', + }); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.ts b/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.ts new file mode 100644 index 0000000000..ab249e9aae --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/action-hub/settings-modal.component.ts @@ -0,0 +1,51 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkyColorpickerModule } from '@skyux/colorpicker'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { MODAL_TITLE } from './modal-title-token'; + +@Component({ + selector: 'app-settings-modal', + templateUrl: './settings-modal.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyColorpickerModule, + SkyInputBoxModule, + SkyModalModule, + ], +}) +export class SettingsModalComponent { + protected formGroup: FormGroup; + protected fields: string[] = []; + + protected readonly modalInstance = inject(SkyModalInstance); + protected readonly title = inject(MODAL_TITLE); + readonly #formBuilder = inject(FormBuilder); + + constructor() { + const controls: Record = {}; + + for (let i = 1; i <= 5; i++) { + const field = `${this.title} ${i}`; + this.fields.push(field); + controls[field] = this.#formBuilder.control(''); + } + + this.formGroup = this.#formBuilder.group(controls); + + this.modalInstance.closed.subscribe((args) => { + if (args.reason === 'save') { + console.log(this.formGroup.value); + } + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.html b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.html new file mode 100644 index 0000000000..d11321718f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + Help + + + Support + + + Training + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.spec.ts new file mode 100644 index 0000000000..7de5d3b257 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.spec.ts @@ -0,0 +1,55 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { SkyHelpTestingModule } from '@skyux/core/testing'; +import { SkyPageHarness } from '@skyux/pages/testing'; + +import { PagesPageHomePageBlocksLayoutExampleComponent } from './example.component'; + +describe('Record page blocks layout example', () => { + async function setupTest(): Promise<{ + pageHarness: SkyPageHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + PagesPageHomePageBlocksLayoutExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const pageHarness = await loader.getHarness(SkyPageHarness); + + return { pageHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + PagesPageHomePageBlocksLayoutExampleComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + providers: [provideRouter([])], + }); + }); + + it('should have a blocks layout', async () => { + const { pageHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(pageHarness.getLayout()).toBeResolvedTo('blocks'); + }); + + it('should have the correct page header text', async () => { + const { pageHarness } = await setupTest(); + + const pageHeaderHarness = await pageHarness.getPageHeader(); + + await expectAsync(pageHeaderHarness.getPageTitle()).toBeResolvedTo('Home'); + + await expectAsync( + pageHeaderHarness.getParentLinkText(), + ).toBeRejectedWithError(/No parent link was found in the page header/); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.ts new file mode 100644 index 0000000000..008044ac2f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/example.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyPageModule, SkyRecentLink } from '@skyux/pages'; + +import { HomePageContentComponent } from './home-page-content.component'; + +@Component({ + selector: 'app-pages-page-home-page-blocks-layout-example', + templateUrl: './example.component.html', + imports: [HomePageContentComponent, SkyIconModule, SkyPageModule], +}) +export class PagesPageHomePageBlocksLayoutExampleComponent { + protected readonly recentLinks: SkyRecentLink[] = [ + { + label: 'Gift Management', + permalink: { url: '' }, + lastAccessed: new Date(2024, 1, 1), + }, + { + label: 'Reporting', + permalink: { url: '' }, + lastAccessed: new Date(2024, 1, 2), + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.html b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.html new file mode 100644 index 0000000000..a4b9695028 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.ts new file mode 100644 index 0000000000..faf6c34d6d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/home-page-content.component.ts @@ -0,0 +1,58 @@ +import { NgClass, NgFor } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyTileDashboardConfig, SkyTilesModule } from '@skyux/tiles'; + +import { TileMyActionsComponent } from './tile-my-actions.component'; +import { TileUpdatesComponent } from './tile-updates.component'; + +@Component({ + selector: 'app-home-page-content', + templateUrl: './home-page-content.component.html', + imports: [NgClass, NgFor, SkyTilesModule], +}) +export class HomePageContentComponent { + protected dashboardConfig: SkyTileDashboardConfig = { + tiles: [ + { + id: 'tile-updates', + componentType: TileUpdatesComponent, + }, + { + id: 'tile-my-actions', + componentType: TileMyActionsComponent, + }, + ], + layout: { + singleColumn: { + tiles: [ + { + id: 'tile-updates', + isCollapsed: false, + }, + { + id: 'tile-my-actions', + isCollapsed: false, + }, + ], + }, + multiColumn: [ + { + tiles: [ + { + id: 'tile-updates', + isCollapsed: false, + }, + ], + }, + { + tiles: [ + { + id: 'tile-my-actions', + isCollapsed: false, + }, + ], + }, + ], + }, + }; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.html b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.html new file mode 100644 index 0000000000..1fc532e9f1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.html @@ -0,0 +1,54 @@ + + My actions + 3 + + + + @for (item of items; track item) { + + @if (item.title) { + + +
{{ item.date }}
+
+ } + + + + + + + + + + + + +
+ } +
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.ts new file mode 100644 index 0000000000..ee5a628a85 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-my-actions.component.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyDropdownModule } from '@skyux/popovers'; +import { SkyTilesModule } from '@skyux/tiles'; + +@Component({ + selector: 'app-tile-my-actions', + styles: ` + :host { + display: block; + } + `, + templateUrl: './tile-my-actions.component.html', + imports: [SkyTilesModule, SkyDropdownModule, SkyRepeaterModule], +}) +export class TileMyActionsComponent { + protected items: { + date: string; + status?: string; + title?: string; + accessibilityLabel?: string; + }[] = [ + { + title: 'Send invitation to Spring Ball', + date: 'Today', + }, + { + title: 'Review portal activity', + date: '10/2/2024', + }, + { + title: 'Assign prospects', + date: '10/3/2024', + }, + ]; + protected onActionClicked(buttonText: string): void { + alert(buttonText + ' was clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.html b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.html new file mode 100644 index 0000000000..5ea07ad05b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.html @@ -0,0 +1,28 @@ + + Updates + 2 + + +

+ Prepare for Giving Tuesday +

+
10/1/2025
+
+ Tips for creating a clear and compelling message, leveraging social + media and email marketing, and providing multiple ways for donors to + give. +
+
+ +

+ What's new +

+
9/28/2025
+
    +
  • Enhancement visualizations in reporting capabilities
  • +
  • New automation for routine tasks
  • +
  • Scan attachments for viruses
  • +
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.ts new file mode 100644 index 0000000000..ebc288048f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/home-page-blocks-layout-demo/tile-updates.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { SkyTilesModule } from '@skyux/tiles'; + +@Component({ + selector: 'app-tile-updates', + styles: ` + :host { + display: block; + } + `, + templateUrl: './tile-updates.component.html', + imports: [SkyTilesModule], +}) +export class TileUpdatesComponent {} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.html new file mode 100644 index 0000000000..a108fb486c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts new file mode 100644 index 0000000000..c64f6e9015 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/dashboards-grid-context-menu.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { Item } from './item'; + +@Component({ + selector: 'app-dashboards-grid-context-menu', + templateUrl: './dashboards-grid-context-menu.component.html', + imports: [SkyDropdownModule], +}) +export class DashboardGridContextMenuComponent + implements ICellRendererAngularComp +{ + protected dashboardName = ''; + + public agInit(params: ICellRendererParams): void { + this.dashboardName = params.data?.dashboard ?? ''; + } + + public refresh(): boolean { + return false; + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.html new file mode 100644 index 0000000000..ee25bf7978 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.spec.ts new file mode 100644 index 0000000000..40609f13cb --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.spec.ts @@ -0,0 +1,60 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyPageHarness } from '@skyux/pages/testing'; + +import { PagesPageListPageListLayoutExampleComponent } from './example.component'; + +describe('List page list layout example', () => { + async function setupTest(): Promise<{ + pageHarness: SkyPageHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + PagesPageListPageListLayoutExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { pageHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + PagesPageListPageListLayoutExampleComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + }); + }); + + it('should have a list layout', async () => { + const { pageHarness } = await setupTest(); + + await expectAsync(pageHarness.getLayout()).toBeResolvedTo('list'); + }); + + it('should have the correct page header text', async () => { + const { pageHarness } = await setupTest(); + + const pageHeaderHarness = await pageHarness.getPageHeader(); + + await expectAsync(pageHeaderHarness.getPageTitle()).toBeResolvedTo( + 'Dashboards', + ); + }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('example-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.ts new file mode 100644 index 0000000000..f86604ee34 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/example.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SkyPageModule } from '@skyux/pages'; + +import { ListPageContentComponent } from './list-page-content.component'; + +@Component({ + selector: 'app-pages-page-list-page-list-layout-example', + templateUrl: './example.component.html', + imports: [ListPageContentComponent, SkyPageModule], +}) +export class PagesPageListPageListLayoutExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/item.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/item.ts new file mode 100644 index 0000000000..fdbea72423 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/item.ts @@ -0,0 +1,5 @@ +export interface Item { + dashboard: string; + name: string; + lastUpdated: string; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.html new file mode 100644 index 0000000000..12dba334a9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.html @@ -0,0 +1,29 @@ + +
+ + + + + +
+
+ + + {{ items.length }} + + Dashboards + +
+ + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.ts new file mode 100644 index 0000000000..aff7249726 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-list-layout-demo/list-page-content.component.ts @@ -0,0 +1,133 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService } from '@skyux/ag-grid'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, +} from '@skyux/data-manager'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyKeyInfoModule } from '@skyux/indicators'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'; + +import { DashboardGridContextMenuComponent } from './dashboards-grid-context-menu.component'; +import { Item } from './item'; + +@Component({ + selector: 'app-list-page-content', + templateUrl: './list-page-content.component.html', + providers: [SkyDataManagerService], + imports: [ + AgGridModule, + SkyAgGridModule, + SkyDataManagerModule, + SkyIconModule, + SkyKeyInfoModule, + ], +}) +export class ListPageContentComponent implements OnInit { + protected items: Item[] = [ + { + dashboard: 'Cash Flow Tracker', + name: 'Kanesha Hutto', + lastUpdated: '06/21/2023', + }, + { + dashboard: 'Accounts Receivable Dashboard', + name: 'Kristeen Lunsford', + lastUpdated: '06/30/2023', + }, + { + dashboard: 'Accounts Payable Dashboard', + name: 'Darcel Lenz', + lastUpdated: '04/20/2023', + }, + { + dashboard: 'Budget vs. Actual', + name: 'Barbara Durr', + lastUpdated: '12/04/2023', + }, + { + dashboard: 'Balance Sheet - New', + name: 'Ilene Woo', + lastUpdated: '12/20/2023', + }, + { + dashboard: 'Debt Management', + name: 'Tonja Sanderson', + lastUpdated: '09/10/2023', + }, + ]; + + protected gridOptions: GridOptions; + + #columnDefs: ColDef[] = [ + { + colId: 'contextMenu', + headerName: '', + sortable: false, + cellRenderer: DashboardGridContextMenuComponent, + maxWidth: 55, + }, + { + colId: 'dashboard', + field: 'dashboard', + headerName: 'Name', + width: 150, + cellRenderer: (params: ICellRendererParams): string => { + return `${params.value}`; + }, + }, + { + colId: 'name', + field: 'name', + headerName: 'Created By', + }, + { + colId: 'lastUpdated', + field: 'lastUpdated', + headerName: 'Last Updated', + }, + ]; + + #viewConfig: SkyDataViewConfig = { + id: 'gridView', + name: 'Grid View', + searchEnabled: true, + }; + + readonly #dataManagerService = inject(SkyDataManagerService); + readonly #agGridSvc = inject(SkyAgGridService); + + constructor() { + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + }, + }); + } + + public ngOnInit(): void { + this.#dataManagerService.initDataManager({ + activeViewId: 'gridView', + dataManagerConfig: {}, + defaultDataState: new SkyDataManagerState({ + views: [ + { + viewId: 'gridView', + displayedColumnIds: [ + 'contextMenu', + 'dashboard', + 'name', + 'lastUpdated', + ], + }, + ], + }), + }); + + this.#dataManagerService.initDataView(this.#viewConfig); + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.html new file mode 100644 index 0000000000..9e4bcf9247 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts new file mode 100644 index 0000000000..bbeb1291c6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact-context-menu.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { Contact } from './contact'; + +@Component({ + selector: 'app-contacts-grid-context-menu', + templateUrl: './contact-context-menu.component.html', + imports: [SkyDropdownModule], +}) +export class ContactContextMenuComponent implements ICellRendererAngularComp { + protected contactName = ''; + + public agInit(params: ICellRendererParams): void { + this.contactName = params.data?.name ?? ''; + } + + public refresh(): boolean { + return false; + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact.ts new file mode 100644 index 0000000000..9c2dc362e4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/contact.ts @@ -0,0 +1,5 @@ +export interface Contact { + name: string; + organization: string; + emailAddress: string; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.html new file mode 100644 index 0000000000..24bee53f45 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.spec.ts new file mode 100644 index 0000000000..2725c55fe0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.spec.ts @@ -0,0 +1,62 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyPageHarness } from '@skyux/pages/testing'; + +import { PagesPageListPageTabsLayoutExampleComponent } from './example.component'; + +describe('List page tabs layout example', () => { + async function setupTest(): Promise<{ + pageHarness: SkyPageHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + PagesPageListPageTabsLayoutExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { pageHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + PagesPageListPageTabsLayoutExampleComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + providers: [provideRouter([])], + }); + }); + + it('should have a tabs layout', async () => { + const { pageHarness } = await setupTest(); + + await expectAsync(pageHarness.getLayout()).toBeResolvedTo('tabs'); + }); + + it('should have the correct page header text', async () => { + const { pageHarness } = await setupTest(); + + const pageHeaderHarness = await pageHarness.getPageHeader(); + + await expectAsync(pageHeaderHarness.getPageTitle()).toBeResolvedTo( + 'Contacts', + ); + }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('example-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.ts new file mode 100644 index 0000000000..d0ee750b9e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/example.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SkyPageModule } from '@skyux/pages'; + +import { ListPageContentComponent } from './list-page-content.component'; + +@Component({ + selector: 'app-pages-page-list-page-tabs-layout-example', + templateUrl: './example.component.html', + imports: [ListPageContentComponent, SkyPageModule], +}) +export class PagesPageListPageTabsLayoutExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.html new file mode 100644 index 0000000000..44afe01cc0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.html @@ -0,0 +1,29 @@ + +
+ + + + + +
+
+ + + {{ contacts.length }} + + Contacts + +
+ + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.ts new file mode 100644 index 0000000000..fbfbca882d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-contacts-grid.component.ts @@ -0,0 +1,115 @@ +import { Component, Input, OnInit, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService } from '@skyux/ag-grid'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, +} from '@skyux/data-manager'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyKeyInfoModule } from '@skyux/indicators'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'; + +import { ContactContextMenuComponent } from './contact-context-menu.component'; + +interface Contact { + name: string; + organization: string; + emailAddress: string; +} + +@Component({ + selector: 'app-list-page-contacts-grid', + templateUrl: './list-page-contacts-grid.component.html', + providers: [SkyDataManagerService], + imports: [ + AgGridModule, + SkyAgGridModule, + SkyDataManagerModule, + SkyIconModule, + SkyKeyInfoModule, + ], +}) +export class ListPageContactsGridComponent implements OnInit { + @Input() + public contacts: Contact[] = []; + + protected gridOptions: GridOptions; + + #dataManagerService = inject(SkyDataManagerService); + #agGridSvc = inject(SkyAgGridService); + + #columnDefs: ColDef[] = [ + { + colId: 'contextMenu', + headerName: '', + sortable: false, + cellRenderer: ContactContextMenuComponent, + maxWidth: 55, + }, + { + colId: 'name', + field: 'name', + headerName: 'Name', + width: 150, + cellRenderer: (params: ICellRendererParams): string => { + return `${params.value}`; + }, + }, + { + colId: 'organization', + field: 'organization', + headerName: 'Organization', + }, + { + colId: 'emailAddress', + field: 'emailAddress', + headerName: 'Email Address', + }, + ]; + + #viewConfig: SkyDataViewConfig = { + id: 'gridView', + name: 'Grid View', + searchEnabled: true, + columnPickerEnabled: true, + columnOptions: [ + { id: 'contextMenu', label: 'Context menu', alwaysDisplayed: true }, + { id: 'name', label: 'Name' }, + { id: 'organization', label: 'Organization' }, + { id: 'emailAddress', label: 'Email Address' }, + ], + }; + + constructor() { + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + }, + }); + } + + public ngOnInit(): void { + this.#dataManagerService.initDataManager({ + activeViewId: 'gridView', + dataManagerConfig: {}, + defaultDataState: new SkyDataManagerState({ + views: [ + { + viewId: 'gridView', + displayedColumnIds: [ + 'contextMenu', + 'name', + 'organization', + 'emailAddress', + ], + }, + ], + }), + }); + + this.#dataManagerService.initDataView(this.#viewConfig); + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.html b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.html new file mode 100644 index 0000000000..b3ed038bac --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.html @@ -0,0 +1,12 @@ + + + @if (activeTabIndex === 0) { + + } + + + @if (activeTabIndex === 1) { + + } + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts new file mode 100644 index 0000000000..c2e2486fee --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/list-page-tabs-layout-demo/list-page-content.component.ts @@ -0,0 +1,76 @@ +import { Component } from '@angular/core'; +import { SkyTabIndex, SkyTabsModule } from '@skyux/tabs'; + +import { Contact } from './contact'; +import { ListPageContactsGridComponent } from './list-page-contacts-grid.component'; + +@Component({ + selector: 'app-list-page-content', + templateUrl: './list-page-content.component.html', + imports: [ListPageContactsGridComponent, SkyTabsModule], +}) +export class ListPageContentComponent { + protected activeTabIndex: SkyTabIndex = 0; + + protected myContacts: Contact[] = [ + { + name: 'Wonda Lumpkin', + organization: 'Riverfront College of the Arts', + emailAddress: 'wlumpkin@yahoo.com', + }, + { + name: 'Eliza Vanhorn', + organization: 'Summit School of the Arts', + emailAddress: 'evanhorn@outlook.com', + }, + { + name: 'Ed Sipes', + organization: 'Reflections Middle School', + emailAddress: 'esipes@yahoo.com', + }, + { + name: 'Elwood Farris', + organization: 'Sandy Lagoon College', + emailAddress: 'elfarris@gmail.com', + }, + { + name: 'Cristen Sizemore', + organization: 'Grafton Vision Health', + emailAddress: 'cristen.sizemore@aol.com', + }, + { + name: 'Latrice Ashmore', + organization: 'Food Bank of Rapid City', + emailAddress: 'lashmore@gmail.com', + }, + ]; + + protected allContacts = [ + ...this.myContacts, + { + name: 'Kanesha Hutto', + organization: 'Los Angeles College of the Arts', + emailAddress: 'khutto@yahoo.com', + }, + { + name: 'Kristeen Lunsford', + organization: 'Food Bank of Los Angeles', + emailAddress: 'kristeen.lunsford@yahoo.com', + }, + { + name: 'Barbara Durr', + organization: 'Riverfront Middle School', + emailAddress: 'bdurr@gmail.com', + }, + { + name: 'Ilene Woo', + organization: 'Rapid City High School', + emailAddress: 'ilene.woo@aol.com', + }, + { + name: 'Darcel Lenz', + organization: 'Riverfront College of the Arts', + emailAddress: 'dlenz@yahoo.com', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.html new file mode 100644 index 0000000000..8ef07fe9f1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.html @@ -0,0 +1,30 @@ + + + + + + + + + + Analysis + + + Tools + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.spec.ts new file mode 100644 index 0000000000..3fcf6dcedf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.spec.ts @@ -0,0 +1,68 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyPageHarness } from '@skyux/pages/testing'; + +import { PagesPageRecordPageBlocksLayoutExampleComponent } from './example.component'; + +describe('Record page blocks layout example', () => { + async function setupTest(): Promise<{ + pageHarness: SkyPageHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + PagesPageRecordPageBlocksLayoutExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { pageHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + PagesPageRecordPageBlocksLayoutExampleComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + providers: [provideRouter([])], + }); + }); + + it('should have a blocks layout', async () => { + const { pageHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(pageHarness.getLayout()).toBeResolvedTo('blocks'); + }); + + it('should have the correct page header text', async () => { + const { pageHarness } = await setupTest(); + + const pageHeaderHarness = await pageHarness.getPageHeader(); + + await expectAsync(pageHeaderHarness.getPageTitle()).toBeResolvedTo( + '$500 pledge', + ); + + await expectAsync(pageHeaderHarness.getParentLinkText()).toBeResolvedTo( + 'Pledges', + ); + }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('example-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.ts new file mode 100644 index 0000000000..0b8dfbf67e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/example.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { SkyPageModule } from '@skyux/pages'; + +import { RecordPageContentComponent } from './record-page-content.component'; + +@Component({ + selector: 'app-pages-page-record-page-blocks-layout-example', + templateUrl: './example.component.html', + imports: [RecordPageContentComponent, SkyPageModule], +}) +export class PagesPageRecordPageBlocksLayoutExampleComponent { + protected readonly recentLinks = [ + { + label: 'Gift Management', + permalink: { url: '' }, + lastAccessed: new Date(2024, 1, 1), + }, + { + label: 'Reporting', + permalink: { url: '' }, + lastAccessed: new Date(2024, 1, 2), + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.html new file mode 100644 index 0000000000..cbf1043cb6 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.html @@ -0,0 +1,93 @@ + + + + + + + + + + @for (recordDetail of recordDetails; track recordDetail) { + + + {{ recordDetail.detail }} + + + {{ recordDetail.info }} + + + } + + + + + + + +
+
+

Actuals

+ @for (actual of actualPayments; track actual; let i = $index) { + + + {{ actual.value }} + + {{ actual.category }} + + } +
+
+

Projected

+ @for ( + projected of projectedPayments; + track projected; + let i = $index + ) { + + + {{ projected.value }} + + + {{ projected.category }} + + + } +
+
+
+
+
+ + + + + @for (activity of recentActivity; track activity) { + + +
+ {{ activity.activity }} +
+
{{ activity.date }}
+
+
+ } +
+
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.scss b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.scss new file mode 100644 index 0000000000..9a298dcf73 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.scss @@ -0,0 +1,13 @@ +.box-2-content { + display: flex; + + div { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .box-2-actuals { + margin-right: 60px; + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.ts new file mode 100644 index 0000000000..6d0dc792e5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-blocks-layout-demo/record-page-content.component.ts @@ -0,0 +1,98 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyKeyInfoModule } from '@skyux/indicators'; +import { + SkyBoxModule, + SkyDescriptionListModule, + SkyFluidGridModule, +} from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; + +@Component({ + selector: 'app-record-page-content', + templateUrl: './record-page-content.component.html', + styleUrls: ['./record-page-content.component.scss'], + imports: [ + CommonModule, + SkyBoxModule, + SkyDescriptionListModule, + SkyFluidGridModule, + SkyIconModule, + SkyKeyInfoModule, + SkyRepeaterModule, + ], +}) +export class RecordPageContentComponent { + protected recordDetails = [ + { + detail: 'Designation', + info: 'General operating', + }, + { + detail: 'Source', + info: 'Online donation form', + }, + { + detail: 'Status', + info: 'Active', + }, + { + detail: 'Due date', + info: '12/12/2023', + }, + { + detail: 'Create date', + info: '01/05/2023', + }, + { + detail: 'Frequency', + info: 'Quarterly', + }, + ]; + + protected actualPayments = [ + { + category: 'Amount', + value: '$845.00', + }, + { + category: 'Assigned', + value: '$800.00', + }, + { + category: 'Applied', + value: '$800.00', + }, + { + category: 'Payments', + value: 25, + }, + ]; + + protected projectedPayments = [ + { + category: 'Amount', + value: '$0', + }, + { + category: 'Line items', + value: 0, + }, + ]; + + protected recentActivity = [ + { + activity: '$250.00 payment processed successfully.', + date: '07/01/2023 12:02 am', + }, + { + activity: '$150.00 payment processed successfully.', + date: '06/15/2023 12:02 am', + }, + { + activity: '$250.00 payment processed successfully.', + date: '04/01/2023 12:02 am', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachment.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachment.ts new file mode 100644 index 0000000000..746fdbb3c4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachment.ts @@ -0,0 +1,6 @@ +export interface Attachment { + name: string; + description: string; + size: string; + dateAdded: string; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.html new file mode 100644 index 0000000000..0b8908e0fa --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.html @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts new file mode 100644 index 0000000000..33656a8e5b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/attachments-grid-context-menu.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { ICellRendererAngularComp } from 'ag-grid-angular'; +import { ICellRendererParams } from 'ag-grid-community'; + +import { Attachment } from './attachment'; + +@Component({ + selector: 'app-attachments-grid-context-menu', + templateUrl: './attachments-grid-context-menu.component.html', + imports: [SkyDropdownModule], +}) +export class AttachmentsGridContextMenuComponent + implements ICellRendererAngularComp +{ + protected attachmentName = ''; + + public agInit(params: ICellRendererParams): void { + this.attachmentName = params.data?.name ?? ''; + } + + public refresh(): boolean { + return false; + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/detail.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/detail.ts new file mode 100644 index 0000000000..158a7675bd --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/detail.ts @@ -0,0 +1,4 @@ +export interface Detail { + detail: string; + info: string; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.html new file mode 100644 index 0000000000..97ecb35137 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.html @@ -0,0 +1,28 @@ + + + + + Missing phone number for Charlene Conners. + + + + + + + + VIP + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.spec.ts new file mode 100644 index 0000000000..09523b5c6d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.spec.ts @@ -0,0 +1,64 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyPageHarness } from '@skyux/pages/testing'; + +import { PagesPageRecordPageTabsLayoutExampleComponent } from './example.component'; + +describe('Record page tabs layout example', () => { + async function setupTest(): Promise<{ + pageHarness: SkyPageHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + PagesPageRecordPageTabsLayoutExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { pageHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + PagesPageRecordPageTabsLayoutExampleComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + providers: [provideRouter([])], + }); + }); + + it('should have a tabs layout', async () => { + const { pageHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(pageHarness.getLayout()).toBeResolvedTo('tabs'); + }); + + it('should have the correct page header text', async () => { + const { pageHarness } = await setupTest(); + + const pageHeaderHarness = await pageHarness.getPageHeader(); + + await expectAsync(pageHeaderHarness.getPageTitle()).toBeResolvedTo( + 'Charlene Conners', + ); + }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('example-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.ts new file mode 100644 index 0000000000..1f87f87754 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/example.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { SkyAvatarModule } from '@skyux/avatar'; +import { SkyAlertModule, SkyLabelModule } from '@skyux/indicators'; +import { SkyPageModule } from '@skyux/pages'; + +import { RecordPageContentComponent } from './record-page-content.component'; + +@Component({ + selector: 'app-pages-page-record-page-tabs-layout-example', + templateUrl: './example.component.html', + imports: [ + RecordPageContentComponent, + SkyAlertModule, + SkyAvatarModule, + SkyLabelModule, + SkyPageModule, + ], +}) +export class PagesPageRecordPageTabsLayoutExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.html new file mode 100644 index 0000000000..10bda74c30 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.html @@ -0,0 +1,29 @@ + +
+ + + + + +
+
+ + + {{ items.length }} + + Attachments + +
+ + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts new file mode 100644 index 0000000000..58e1fb3fc7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-attachments-tab.component.ts @@ -0,0 +1,120 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService } from '@skyux/ag-grid'; +import { + SkyDataManagerModule, + SkyDataManagerService, + SkyDataManagerState, + SkyDataViewConfig, +} from '@skyux/data-manager'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyKeyInfoModule } from '@skyux/indicators'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'; + +import { Attachment } from './attachment'; +import { AttachmentsGridContextMenuComponent } from './attachments-grid-context-menu.component'; + +@Component({ + selector: 'app-record-page-attachments-tab', + templateUrl: './record-page-attachments-tab.component.html', + providers: [SkyDataManagerService], + imports: [ + AgGridModule, + SkyAgGridModule, + SkyDataManagerModule, + SkyKeyInfoModule, + SkyIconModule, + ], +}) +export class RecordPageAttachmentsTabComponent implements OnInit { + protected items: Attachment[] = [ + { + name: 'Agreement.pdf', + description: 'Cardholder agreement', + size: '10 KB', + dateAdded: '01/28/2023', + }, + { + name: 'Appendix.pdf', + description: 'Updated terms 2023', + size: '25 KB', + dateAdded: '05/05/2023', + }, + ]; + protected gridOptions: GridOptions; + + #dataManagerService = inject(SkyDataManagerService); + #agGridSvc = inject(SkyAgGridService); + + #columnDefs: ColDef[] = [ + { + colId: 'contextMenu', + headerName: '', + sortable: false, + cellRenderer: AttachmentsGridContextMenuComponent, + maxWidth: 55, + }, + { + colId: 'name', + field: 'name', + headerName: 'Name', + width: 150, + cellRenderer: (params: ICellRendererParams): string => { + return `${params.value}`; + }, + }, + { + colId: 'description', + field: 'description', + headerName: 'Description', + }, + { + colId: 'size', + field: 'size', + headerName: 'Size', + }, + { + colId: 'dateAdded', + field: 'dateAdded', + headerName: 'Date Added', + }, + ]; + + #viewConfig: SkyDataViewConfig = { + id: 'gridView', + name: 'Grid View', + searchEnabled: true, + }; + + constructor() { + this.gridOptions = this.#agGridSvc.getGridOptions({ + gridOptions: { + columnDefs: this.#columnDefs, + }, + }); + } + + public ngOnInit(): void { + this.#dataManagerService.initDataManager({ + activeViewId: 'gridView', + dataManagerConfig: {}, + defaultDataState: new SkyDataManagerState({ + views: [ + { + viewId: 'gridView', + displayedColumnIds: [ + 'contextMenu', + 'name', + 'description', + 'size', + 'dateAdded', + ], + }, + ], + }), + }); + + this.#dataManagerService.initDataView(this.#viewConfig); + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.html new file mode 100644 index 0000000000..bb5d94c9cf --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.html @@ -0,0 +1,13 @@ + + + + + + + + + @if (activeTabIndex === 2) { + + } + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.ts new file mode 100644 index 0000000000..c99db73b8e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-content.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { SkyTabIndex, SkyTabsModule } from '@skyux/tabs'; + +import { RecordPageAttachmentsTabComponent } from './record-page-attachments-tab.component'; +import { RecordPageNotesTabComponent } from './record-page-notes-tab.component'; +import { RecordPageOverviewTabComponent } from './record-page-overview-tab.component'; + +@Component({ + selector: 'app-record-page-content', + templateUrl: './record-page-content.component.html', + imports: [ + RecordPageAttachmentsTabComponent, + RecordPageNotesTabComponent, + RecordPageOverviewTabComponent, + SkyTabsModule, + ], +}) +export class RecordPageContentComponent { + protected activeTabIndex: SkyTabIndex = 0; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.html new file mode 100644 index 0000000000..b963b7c954 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.html @@ -0,0 +1,92 @@ +
+ + + + + + + + + Date created (newest first) + + + + + Date created (oldest first) + + + + + + + + + + + +
+
+ + + {{ notes.length }} + + Notes + +
+ + @for (note of notes; track note) { + + + + + + + + + + + + + + +
+ {{ note.content }} +
+
{{ note.date }}
+
+
+ } +
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.ts new file mode 100644 index 0000000000..72b2a7e069 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-notes-tab.component.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyKeyInfoModule } from '@skyux/indicators'; +import { SkyToolbarModule } from '@skyux/layout'; +import { SkyRepeaterModule, SkySortModule } from '@skyux/lists'; +import { SkySearchModule } from '@skyux/lookup'; +import { SkyDropdownModule } from '@skyux/popovers'; + +@Component({ + selector: 'app-record-page-notes-tab', + templateUrl: './record-page-notes-tab.component.html', + imports: [ + SkyDropdownModule, + SkyIconModule, + SkyKeyInfoModule, + SkyRepeaterModule, + SkySearchModule, + SkySortModule, + SkyToolbarModule, + ], +}) +export class RecordPageNotesTabComponent { + protected notes = [ + { + noteNumber: 1, + content: 'Attended our gala last year and had a great time.', + date: '11/17/2024', + }, + { + noteNumber: 2, + content: + 'Is a business owner and has a lot of connections in the community.', + date: '10/11/2024', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.html b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.html new file mode 100644 index 0000000000..8078694203 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.html @@ -0,0 +1,93 @@ + + + + + + + + + + @for (recordDetail of recordDetails; track recordDetail) { + + + {{ recordDetail.detail }} + + + {{ recordDetail.info }} + + + } + + + + + + + +
+
+

Actuals

+ @for (actual of actualPayments; track actual; let i = $index) { + + + {{ actual.value }} + + {{ actual.category }} + + } +
+
+

Projected

+ @for ( + projected of projectedPayments; + track projected; + let i = $index + ) { + + + {{ projected.value }} + + + {{ projected.category }} + + + } +
+
+
+
+
+ + + + + @for (activity of recentActivity; track activity) { + + +
+ {{ activity.activity }} +
+
{{ activity.date }}
+
+
+ } +
+
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.scss b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.scss new file mode 100644 index 0000000000..9a298dcf73 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.scss @@ -0,0 +1,13 @@ +.box-2-content { + display: flex; + + div { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .box-2-actuals { + margin-right: 60px; + } +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts new file mode 100644 index 0000000000..3c0e537bb5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/record-page-tabs-layout-demo/record-page-overview-tab.component.ts @@ -0,0 +1,100 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyKeyInfoModule } from '@skyux/indicators'; +import { + SkyBoxModule, + SkyDescriptionListModule, + SkyFluidGridModule, +} from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; + +import { Detail } from './detail'; + +@Component({ + selector: 'app-record-page-overview-tab', + templateUrl: './record-page-overview-tab.component.html', + styleUrls: ['./record-page-overview-tab.component.scss'], + imports: [ + CommonModule, + SkyBoxModule, + SkyDescriptionListModule, + SkyFluidGridModule, + SkyIconModule, + SkyKeyInfoModule, + SkyRepeaterModule, + ], +}) +export class RecordPageOverviewTabComponent { + protected recordDetails: Detail[] = [ + { + detail: 'Designation', + info: 'General operating', + }, + { + detail: 'Source', + info: 'Online donation form', + }, + { + detail: 'Status', + info: 'Active', + }, + { + detail: 'Due date', + info: '12/12/2023', + }, + { + detail: 'Create date', + info: '01/05/2023', + }, + { + detail: 'Frequency', + info: 'Quarterly', + }, + ]; + + protected actualPayments = [ + { + category: 'Amount', + value: '$845.00', + }, + { + category: 'Assigned', + value: '$800.00', + }, + { + category: 'Applied', + value: '$800.00', + }, + { + category: 'Payments', + value: 25, + }, + ]; + + protected projectedPayments = [ + { + category: 'Amount', + value: '$0', + }, + { + category: 'Line items', + value: 0, + }, + ]; + + protected recentActivity = [ + { + activity: '$250.00 payment processed successfully.', + date: '07/01/2023 12:02 am', + }, + { + activity: '$150.00 payment processed successfully.', + date: '06/15/2023 12:02 am', + }, + { + activity: '$250.00 payment processed successfully.', + date: '04/01/2023 12:02 am', + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.html b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.html new file mode 100644 index 0000000000..051ec22e2c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.html @@ -0,0 +1,25 @@ + + + + + There are multiple expense approvals due soon. + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.spec.ts new file mode 100644 index 0000000000..a6b2a7901a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.spec.ts @@ -0,0 +1,54 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; +import { SkyPageHarness } from '@skyux/pages/testing'; + +import { PagesPageSplitViewPageFitLayoutExampleComponent } from './example.component'; + +describe('Split view page fit layout example', () => { + async function setupTest(): Promise<{ + pageHarness: SkyPageHarness; + fixture: ComponentFixture; + helpController: SkyHelpTestingController; + }> { + const fixture = TestBed.createComponent( + PagesPageSplitViewPageFitLayoutExampleComponent, + ); + + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); + + return { pageHarness, fixture, helpController }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + PagesPageSplitViewPageFitLayoutExampleComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + providers: [provideRouter([])], + }); + }); + + it('should have a fit layout', async () => { + const { pageHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(pageHarness.getLayout()).toBeResolvedTo('fit'); + }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('example-help'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.ts new file mode 100644 index 0000000000..967f679ed5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/example.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { SkyAlertModule } from '@skyux/indicators'; +import { SkyPageModule } from '@skyux/pages'; + +import { SplitViewPageContentComponent } from './split-view-page-content.component'; + +@Component({ + selector: 'app-pages-page-split-view-page-fit-layout-example', + templateUrl: './example.component.html', + imports: [SkyAlertModule, SkyPageModule, SplitViewPageContentComponent], +}) +export class PagesPageSplitViewPageFitLayoutExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.html b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.html new file mode 100644 index 0000000000..ab5d213522 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.html @@ -0,0 +1,84 @@ + + + + @for (item of items; track item; let i = $index) { + + + {{ item.amount }}
+ {{ item.date }}
+ {{ item.vendor }} +
+
+ } +
+
+ + + +
+ + + + Receipt amount + + + {{ activeRecord.amount }} + + + + Date + + {{ activeRecord.date }} + + + + Vendor + + {{ activeRecord.vendor }} + + + + + Receipt image + + + {{ activeRecord.receiptImage }} + + + +
+ + + +
+ +
+ + + +
+
+
+ + + + + Approve expense + + + + Deny expense + + + + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.ts b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.ts new file mode 100644 index 0000000000..88ef6bc37e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/pages/page/split-view-page-fit-layout-demo/split-view-page-content.component.ts @@ -0,0 +1,219 @@ +import { Component, inject } from '@angular/core'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SkySummaryActionBarModule } from '@skyux/action-bars'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyDescriptionListModule } from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyConfirmService, SkyConfirmType } from '@skyux/modals'; +import { + SkySplitViewMessage, + SkySplitViewMessageType, + SkySplitViewModule, +} from '@skyux/split-view'; + +import { Subject } from 'rxjs'; + +interface WorkspaceItem { + id: number; + amount: number; + date: string; + vendor: string; + receiptImage: string; + approvedAmount: number; + comments: string; +} + +@Component({ + selector: 'app-split-view-page-content', + templateUrl: './split-view-page-content.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyDescriptionListModule, + SkyInputBoxModule, + SkyRepeaterModule, + SkySplitViewModule, + SkySummaryActionBarModule, + ], +}) +export class SplitViewPageContentComponent { + protected set activeIndex(value: number) { + this.#_activeIndex = value; + this.activeRecord = this.items[this.#_activeIndex]; + this.#loadFormGroup(this.activeRecord); + } + + protected get activeIndex(): number { + return this.#_activeIndex; + } + + protected items: WorkspaceItem[] = [ + { + id: 1, + amount: 73.19, + date: '5/13/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-13-19.png', + approvedAmount: 73.19, + comments: '', + }, + { + id: 2, + amount: 214.12, + date: '5/14/2020', + vendor: 'Office Max', + receiptImage: 'office-max-order.png', + approvedAmount: 214.12, + comments: '', + }, + { + id: 3, + amount: 29.99, + date: '5/14/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-14-19.png', + approvedAmount: 29.99, + comments: '', + }, + { + id: 4, + amount: 1500, + date: '5/15/2020', + vendor: 'Fresh Catering, LLC', + receiptImage: 'fresh-catering-llc-order.png', + approvedAmount: 1500, + comments: '', + }, + { + id: 5, + amount: 456.24, + date: '5/16/2020', + vendor: 'Wish', + receiptImage: 'wish-delivery-order.png', + approvedAmount: 456.24, + comments: '', + }, + { + id: 6, + amount: 62.37, + date: '5/16/2020', + vendor: 'Staples', + receiptImage: 'staples-paper-bulk-order.png', + approvedAmount: 62.37, + comments: '', + }, + { + id: 7, + amount: 51.84, + date: '5/17/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-17-19.png', + approvedAmount: 51.84, + comments: '', + }, + { + id: 8, + amount: 92.55, + date: '5/18/2020', + vendor: 'Home Depot', + receiptImage: 'home-depot-order.png', + approvedAmount: 0.0, + comments: '', + }, + { + id: 9, + amount: 38.29, + date: '5/18/2020', + vendor: 'Papa Johns', + receiptImage: 'papa-johns-order.png', + approvedAmount: 38.29, + comments: '', + }, + ]; + + protected activeRecord = this.items[0]; + protected listWidth: number | undefined; + protected splitViewStream = new Subject(); + + protected splitViewDemoForm = inject(FormBuilder).group({ + approvedAmount: [this.activeRecord.approvedAmount], + comments: [this.activeRecord.comments], + }); + + #_activeIndex = 0; + + #confirmSvc = inject(SkyConfirmService); + + protected onItemClick(index: number): void { + // Prevent workspace from loading new data if the current workspace form is dirty. + if (this.splitViewDemoForm.dirty && index !== this.activeIndex) { + this.#openConfirmModal(index); + } else { + this.#loadWorkspace(index); + } + } + + protected onApprove(): void { + console.log('Approved clicked!'); + this.#saveForm(); + } + + protected onDeny(): void { + console.log('Denied clicked!'); + } + + #loadFormGroup(record: WorkspaceItem): void { + this.splitViewDemoForm.setValue({ + approvedAmount: record.approvedAmount, + comments: record.comments, + }); + } + + #loadWorkspace(index: number): void { + this.activeIndex = index; + this.#setFocusInWorkspace(); + } + + #openConfirmModal(index: number): void { + this.#confirmSvc + .open({ + message: + 'You have unsaved work. Would you like to save it before you change records?', + type: SkyConfirmType.Custom, + buttons: [ + { + action: 'yes', + text: 'Yes', + styleType: 'primary', + }, + { + action: 'discard', + text: 'Discard changes', + styleType: 'link', + }, + ], + }) + .closed.subscribe((closeArgs) => { + if (closeArgs.action === 'yes') { + this.#saveForm(); + } + + this.#loadWorkspace(index); + }); + } + + #saveForm(): void { + this.activeRecord = Object.assign( + this.activeRecord, + this.splitViewDemoForm.value, + ); + + this.splitViewDemoForm.markAsPristine(); + } + + #setFocusInWorkspace(): void { + this.splitViewStream.next({ + type: SkySplitViewMessageType.FocusWorkspace, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.html b/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.html new file mode 100644 index 0000000000..223dd7b30c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.html @@ -0,0 +1,23 @@ +
+
+ + + + + +
+
+ + + We use this number for calls and text about your student. We won't use it for + other purposes. +
+
+ For more information, see our privacy policy. +
diff --git a/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.spec.ts new file mode 100644 index 0000000000..88e861c466 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.spec.ts @@ -0,0 +1,107 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyPhoneFieldCountry } from '@skyux/phone-field'; +import { SkyPhoneFieldHarness } from '@skyux/phone-field/testing'; + +import { PhoneFieldBasicExampleComponent } from './example.component'; + +const COUNTRY_AU: SkyPhoneFieldCountry = { + name: 'Australia', + iso2: 'au', + dialCode: '+61', +}; + +const DATA_SKY_ID = 'my-phone-field'; +const VALID_AU_NUMBER = '0212345678'; +const VALID_US_NUMBER = '8675555309'; + +describe('Basic phone field example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyPhoneFieldHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent(PhoneFieldBasicExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await ( + await loader.getHarness( + SkyInputBoxHarness.with({ dataSkyId: options.dataSkyId }), + ) + ).queryHarness(SkyPhoneFieldHarness); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoneFieldBasicExampleComponent, NoopAnimationsModule], + }); + }); + + it('should set up phone field input and clear value', async () => { + const { harness } = await setupTest({ + dataSkyId: DATA_SKY_ID, + }); + + // First, set a value on the phoneField. + const inputHarness = await harness.getControl(); + await inputHarness.focus(); + await inputHarness.setValue(VALID_US_NUMBER); + + await expectAsync(inputHarness.getValue()).toBeResolvedTo(VALID_US_NUMBER); + + // Now, clear the value. + await inputHarness.clear(); + await expectAsync(inputHarness.getValue()).toBeResolvedTo(''); + }); + + it('should use selected country', async () => { + const { harness, fixture } = await setupTest({ + dataSkyId: DATA_SKY_ID, + }); + + const inputHarness = await harness.getControl(); + await inputHarness.focus(); + // enter a valid phone number for the default country + await inputHarness.setValue(VALID_US_NUMBER); + + // expect the model to use the proper dial code and format + await expectAsync(inputHarness.getValue()).toBeResolvedTo(VALID_US_NUMBER); + expect(fixture.componentInstance.phoneControl.value).toEqual( + '(867) 555-5309', + ); + + if (COUNTRY_AU.name) { + // change the country + await harness.selectCountry(COUNTRY_AU.name); + } + + const countryName: string | null = await harness.getSelectedCountryName(); + + const countryIos2: string | null = await harness.getSelectedCountryIso2(); + + fixture.detectChanges(); + await fixture.whenStable(); + + if (COUNTRY_AU.name && countryName) { + expect(countryName).toBe(COUNTRY_AU.name); + } + if (COUNTRY_AU.iso2 && countryIos2) { + expect(countryIos2).toBe(COUNTRY_AU.iso2); + } + + // enter a valid phone number for the new country + await inputHarness.setValue(VALID_AU_NUMBER); + + // expect the model to use the proper dial code and format + await expectAsync(inputHarness.getValue()).toBeResolvedTo(VALID_AU_NUMBER); + expect(fixture.componentInstance.phoneControl.value).toEqual( + '+61 2 1234 5678', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.ts new file mode 100644 index 0000000000..949c770336 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/phone-field/phone-field/basic/example.component.ts @@ -0,0 +1,37 @@ +import { Component, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyPhoneFieldModule } from '@skyux/phone-field'; + +@Component({ + selector: 'app-phone-field-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyPhoneFieldModule, + ], +}) +export class PhoneFieldBasicExampleComponent { + public phoneForm: FormGroup; + public phoneControl: FormControl; + + #formBuilder = inject(FormBuilder); + + constructor() { + this.phoneControl = this.#formBuilder.control(undefined, { + validators: Validators.required, + }); + this.phoneForm = this.#formBuilder.group({ + phoneControl: this.phoneControl, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.html b/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.html new file mode 100644 index 0000000000..deb8a9e62a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.html @@ -0,0 +1,32 @@ + + Show dropdown + + @for (item of items; track item) { + + + + } + + +
+ + + @for (item of items; track item) { + + + + } + + diff --git a/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.spec.ts new file mode 100644 index 0000000000..f8038afa1d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.spec.ts @@ -0,0 +1,82 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@skyux-sdk/testing'; +import { SkyDropdownHarness } from '@skyux/popovers/testing'; + +import { PopoversDropdownBasicExampleComponent } from './example.component'; + +describe('Basic dropdown', () => { + async function setupTest(): Promise<{ + dropdownHarness: SkyDropdownHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + PopoversDropdownBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + const dropdownHarness = await loader.getHarness( + SkyDropdownHarness.with({ + dataSkyId: 'dropdown-example', + }), + ); + + return { dropdownHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PopoversDropdownBasicExampleComponent], + }); + }); + + it('should display the correct dropdown', async () => { + const { dropdownHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + + await expectAsync(dropdownHarness.getButtonStyle()).toBeResolvedTo( + 'default', + ); + await expectAsync(dropdownHarness.getButtonType()).toBeResolvedTo('select'); + await expectAsync(dropdownHarness.isDisabled()).toBeResolvedTo(false); + await expectAsync(dropdownHarness.getAriaLabel()).toBeResolvedTo( + 'Test dropdown', + ); + await expectAsync(dropdownHarness.getTitle()).toBeResolvedTo(null); + await expectAsync(dropdownHarness.isOpen()).toBeResolvedTo(false); + }); + + it('should open the correct dropdown menu', async () => { + const { dropdownHarness, fixture } = await setupTest(); + + fixture.detectChanges(); + await dropdownHarness.clickDropdownButton(); + fixture.detectChanges(); + + const dropdownMenu = await dropdownHarness.getDropdownMenu(); + const dropdownMenuItems = await dropdownMenu.getItems(); + + await expectAsync(dropdownHarness.isOpen()).toBeResolvedTo(true); + await expectAsync(dropdownMenu.getAriaRole()).toBeResolvedTo('menu'); + + await expectAsync(dropdownMenuItems?.[0].getText()).toBeResolvedTo( + 'Option 1', + ); + }); + + it('should click the correct dropdown menu item', async () => { + const { dropdownHarness, fixture } = await setupTest(); + + const clickSpy = spyOn(fixture.componentInstance, 'actionClicked'); + fixture.detectChanges(); + await dropdownHarness.clickDropdownButton(); + fixture.detectChanges(); + + const dropdownMenu = await dropdownHarness.getDropdownMenu(); + const dropdownMenuItem = await dropdownMenu.getItem({ text: 'Option 1' }); + + await dropdownMenuItem?.click(); + + expect(clickSpy).toHaveBeenCalledWith('Option 1'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.ts new file mode 100644 index 0000000000..2c7fbda135 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/dropdown/basic/example.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +interface DropdownItem { + name: string; + disabled: boolean; +} + +@Component({ + selector: 'app-popovers-dropdown-basic-example', + templateUrl: './example.component.html', + imports: [SkyDropdownModule], +}) +export class PopoversDropdownBasicExampleComponent { + protected items: DropdownItem[] = [ + { name: 'Option 1', disabled: false }, + { name: 'Disabled option', disabled: true }, + { name: 'Option 3', disabled: false }, + { name: 'Option 4', disabled: false }, + { name: 'Option 5', disabled: false }, + ]; + + public actionClicked(action: string): void { + alert(`You selected ${action}.`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.html b/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.html new file mode 100644 index 0000000000..d26c59fdb3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.html @@ -0,0 +1,14 @@ + + + + {{ popoverBody }} + diff --git a/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.spec.ts new file mode 100644 index 0000000000..d0896d19b4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.spec.ts @@ -0,0 +1,83 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyPopoverAlignment, SkyPopoverPlacement } from '@skyux/popovers'; +import { SkyPopoverHarness } from '@skyux/popovers/testing'; + +import { PopoversPopoverBasicExampleComponent } from './example.component'; + +describe('Basic popover', () => { + async function setupTest(options?: { + titleText?: string; + alignment?: SkyPopoverAlignment; + placement?: SkyPopoverPlacement; + }): Promise<{ + popoverHarness: SkyPopoverHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + PopoversPopoverBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + if (options) { + fixture.componentInstance.popoverAlignment = options.alignment; + fixture.componentInstance.popoverPlacement = options.placement; + fixture.componentInstance.popoverTitle = options.titleText; + } + + fixture.detectChanges(); + await fixture.whenStable(); + + const popoverHarness = await loader.getHarness( + SkyPopoverHarness.with({ + dataSkyId: 'popover-example', + }), + ); + + return { popoverHarness, fixture }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PopoversPopoverBasicExampleComponent, NoopAnimationsModule], + }); + }); + + it('should open and close when the user interacts with the trigger', async () => { + const { popoverHarness } = await setupTest(); + + await expectAsync(popoverHarness.isOpen()).toBeResolvedTo(false); + + await popoverHarness.clickPopoverButton(); + await expectAsync(popoverHarness.isOpen()).toBeResolvedTo(true); + + await popoverHarness.clickPopoverButton(); + await expectAsync(popoverHarness.isOpen()).toBeResolvedTo(false); + }); + + it('should expose content properties when visible', async () => { + const { popoverHarness } = await setupTest({ + titleText: 'Did you know?', + placement: 'right', + }); + + await popoverHarness.clickPopoverButton(); + const contentHarness = await popoverHarness.getPopoverContent(); + + await expectAsync(contentHarness.getTitleText()).toBeResolvedTo( + 'Did you know?', + ); + await expectAsync(contentHarness.getBodyText()).toBeResolvedTo( + 'This is a popover.', + ); + await expectAsync(contentHarness.getAlignment()).toBeResolvedTo('center'); + await expectAsync(contentHarness.getPlacement()).toBeResolvedTo('right'); + + await popoverHarness.clickPopoverButton(); + // Attempting to call this method when the popover is closed will result in an error. + await expectAsync(popoverHarness.getPopoverContent()).toBeRejectedWithError( + 'Unable to retrieve the popover content because the popover is not open.', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.ts new file mode 100644 index 0000000000..d953d9670d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/popover/basic/example.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { + SkyPopoverAlignment, + SkyPopoverModule, + SkyPopoverPlacement, +} from '@skyux/popovers'; + +@Component({ + selector: 'app-popovers-popover-basic-example', + templateUrl: './example.component.html', + imports: [SkyPopoverModule], +}) +export class PopoversPopoverBasicExampleComponent { + public popoverAlignment: SkyPopoverAlignment | undefined; + public popoverBody = 'This is a popover.'; + public popoverPlacement: SkyPopoverPlacement | undefined; + public popoverTitle: string | undefined = 'Did you know?'; +} diff --git a/libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.html b/libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.html new file mode 100644 index 0000000000..84a6916c80 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.html @@ -0,0 +1,21 @@ + + + + + + This is a popover. + diff --git a/libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.ts b/libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.ts new file mode 100644 index 0000000000..b775e0f2a7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/popovers/popover/programmatic/example.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; +import { + SkyPopoverMessage, + SkyPopoverMessageType, + SkyPopoverModule, +} from '@skyux/popovers'; + +import { Subject } from 'rxjs'; + +@Component({ + selector: 'app-popovers-popover-programmatic-example', + templateUrl: './example.component.html', + imports: [SkyHelpInlineModule, SkyPopoverModule], +}) +export class PopoversPopoverProgrammaticExampleComponent { + protected popoverController = new Subject(); + + #popoverOpen = false; + + protected onPopoverStateChange(isOpen: boolean): void { + this.#popoverOpen = isOpen; + } + + protected openPopover(): void { + if (!this.#popoverOpen) { + this.#sendMessage(SkyPopoverMessageType.Open); + } + } + + #sendMessage(type: SkyPopoverMessageType): void { + const message: SkyPopoverMessage = { type }; + this.popoverController.next(message); + } +} diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.html b/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.html new file mode 100644 index 0000000000..42a4d1a445 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.html @@ -0,0 +1,18 @@ + + Optional details about this step + + + + diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.spec.ts new file mode 100644 index 0000000000..ebc9a2c041 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.spec.ts @@ -0,0 +1,40 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyProgressIndicatorHarness } from '@skyux/progress-indicator/testing'; + +import { ProgressIndicatorPassiveIndicatorBasicExampleComponent } from './example.component'; + +describe('Basic passive progress indicator example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyProgressIndicatorHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + ProgressIndicatorPassiveIndicatorBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyProgressIndicatorHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + it('should have the initial progress set', async () => { + const { harness } = await setupTest({ + dataSkyId: 'example-progress-indicator', + }); + + await expectAsync(harness.isPassive()).toBeResolvedTo(true); + expect((await harness.getItems()).length).toBe(3); + + const finishedStep = harness.getItem({ dataSkyId: 'finished-step' }); + await expectAsync((await finishedStep).getTitle()).toBeResolvedTo( + 'Finished step', + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.ts new file mode 100644 index 0000000000..708d630534 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/passive-indicator/basic/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyProgressIndicatorModule } from '@skyux/progress-indicator'; + +@Component({ + selector: 'app-progress-indicator-passive-indicator-basic-example', + templateUrl: './example.component.html', + imports: [SkyProgressIndicatorModule], +}) +export class ProgressIndicatorPassiveIndicatorBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.html b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.html new file mode 100644 index 0000000000..b093eff227 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.html @@ -0,0 +1,113 @@ + + + Set up my connection + + + +
+ @if (activeIndex === 0) { + + } + @if (activeIndex && activeIndex > 0) { + + } +
+
+ + + @if (activeIndex && activeIndex >= 1) { +
+ @if (activeIndex === 1) { + + } @else { + + } +
+ } +
+ + + @if (activeIndex && activeIndex >= 2) { +
+ @if (activeIndex === 2) { + + } @else { + + } +
+ } +
+ + + @if (activeIndex && activeIndex >= 3) { +
+ @if (activeIndex === 3) { + + } +
+ } +
+ + + Erase all settings + +
diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.spec.ts new file mode 100644 index 0000000000..3450b06f31 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.spec.ts @@ -0,0 +1,57 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyProgressIndicatorHarness } from '@skyux/progress-indicator/testing'; + +import { ProgressIndicatorWaterfallIndicatorBasicExampleComponent } from './example.component'; + +describe('Basic passive progress indicator example', () => { + async function setupTest(options: { dataSkyId: string }): Promise<{ + harness: SkyProgressIndicatorHarness; + fixture: ComponentFixture; + }> { + const fixture = TestBed.createComponent( + ProgressIndicatorWaterfallIndicatorBasicExampleComponent, + ); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyProgressIndicatorHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return { harness, fixture }; + } + + it('should have the initial progress set', async () => { + const { harness } = await setupTest({ + dataSkyId: 'example-progress-indicator', + }); + + await expectAsync(harness.getTitle()).toBeResolvedTo( + 'Set up my connection', + ); + + const items = await harness.getItems(); + + expect(items.length).toBe(4); + + const configureStep = harness.getItem({ + dataSkyId: 'configure-connection', + }); + await expectAsync((await configureStep).getTitle()).toBeResolvedTo( + 'Configure connection', + ); + }); + + it('should reset connection setup', async () => { + const { harness } = await setupTest({ + dataSkyId: 'example-progress-indicator', + }); + + await harness.clickResetButton(); + const firstStep = (await harness.getItems())[0]; + await expectAsync(firstStep.isCompleted()).toBeResolvedTo(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.ts new file mode 100644 index 0000000000..1341641e72 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/example.component.ts @@ -0,0 +1,98 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + inject, +} from '@angular/core'; +import { SkyModalCloseArgs, SkyModalService } from '@skyux/modals'; +import { + SkyProgressIndicatorChange, + SkyProgressIndicatorMessage, + SkyProgressIndicatorMessageType, + SkyProgressIndicatorModule, +} from '@skyux/progress-indicator'; + +import { Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { ModalContext } from './modal-context'; +import { ModalComponent } from './modal.component'; + +@Component({ + selector: 'app-progress-indicator-waterfall-indicator-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyProgressIndicatorModule], +}) +export class ProgressIndicatorWaterfallIndicatorBasicExampleComponent { + protected activeIndex: number | undefined = 0; + + protected progressMessageStream = new Subject< + SkyProgressIndicatorMessage | SkyProgressIndicatorMessageType + >(); + + readonly #changeDetectorRef = inject(ChangeDetectorRef); + readonly #modalSvc = inject(SkyModalService); + + protected configureConnection(isProgress: boolean): void { + this.#openModalForm( + { + title: 'Configure connection', + buttonText: 'Submit connection settings', + }, + isProgress, + ); + } + + protected setServer(isProgress: boolean): void { + this.#openModalForm( + { + title: 'Select remote server', + buttonText: 'Submit server choice', + }, + isProgress, + ); + } + + protected testConnection(isProgress: boolean): void { + this.#openModalForm( + { + title: 'Connection confirmed.', + buttonText: 'OK', + }, + isProgress, + ); + } + + protected alertMessage(message: string): void { + alert(message); + } + + protected updateIndex(changes: SkyProgressIndicatorChange): void { + this.activeIndex = changes.activeIndex; + this.#changeDetectorRef.detectChanges(); + } + + protected resetClicked(): void { + this.progressMessageStream.next(SkyProgressIndicatorMessageType.Reset); + } + + private progress(): void { + this.progressMessageStream.next(SkyProgressIndicatorMessageType.Progress); + } + + #openModalForm(context: ModalContext, isProgress: boolean): void { + const modalForm = this.#modalSvc.open(ModalComponent, [ + { + provide: ModalContext, + useValue: context, + }, + ]); + + modalForm.closed.pipe(take(1)).subscribe((args: SkyModalCloseArgs) => { + if (args.reason === 'save' && isProgress) { + this.progress(); + } + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal-context.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal-context.ts new file mode 100755 index 0000000000..25c2700d6b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal-context.ts @@ -0,0 +1,4 @@ +export class ModalContext { + public title = ''; + public buttonText = ''; +} diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.html b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.html new file mode 100755 index 0000000000..483e6c4e44 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.html @@ -0,0 +1,11 @@ + + +
+
+
+ + + +
diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.ts new file mode 100755 index 0000000000..bf8f81f6b1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/waterfall-indicator/basic/modal.component.ts @@ -0,0 +1,18 @@ +import { Component, inject } from '@angular/core'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; + +import { ModalContext } from './modal-context'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [SkyModalModule], +}) +export class ModalComponent { + protected readonly context = inject(ModalContext); + protected readonly instance = inject(SkyModalInstance); + + protected submit(): void { + this.instance.close(undefined, 'save'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.html b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.html new file mode 100644 index 0000000000..06400c0f6a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.html @@ -0,0 +1,3 @@ + diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.ts new file mode 100644 index 0000000000..8093510dab --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/example.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyModalService } from '@skyux/modals'; + +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-progress-indicator-wizard-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProgressIndicatorWizardBasicExampleComponent { + readonly #modalSvc = inject(SkyModalService); + + protected openWizard(): void { + this.#modalSvc.open(ModalComponent); + } +} diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.html b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.html new file mode 100644 index 0000000000..b952f3eeca --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.html @@ -0,0 +1,54 @@ + + +
+ + + + + + + + +
+ +
+
+ + +
Additional content or tasks go here.
+
+
+
+
+ + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.ts b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.ts new file mode 100644 index 0000000000..2665b309c3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/progress-indicator/wizard/basic/modal.component.ts @@ -0,0 +1,61 @@ +import { Component, inject } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { + SkyProgressIndicatorActionClickArgs, + SkyProgressIndicatorChange, + SkyProgressIndicatorDisplayModeType, + SkyProgressIndicatorModule, +} from '@skyux/progress-indicator'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [ + ReactiveFormsModule, + SkyCheckboxModule, + SkyInputBoxModule, + SkyModalModule, + SkyProgressIndicatorModule, + ], +}) +export class ModalComponent { + protected activeIndex: number | undefined = 0; + protected displayMode: SkyProgressIndicatorDisplayModeType = 'horizontal'; + protected formGroup: FormGroup; + protected title = 'Wizard example'; + + protected get requirementsMet(): boolean { + switch (this.activeIndex) { + case 0: + return !!this.formGroup.get('requiredValue1')?.value; + case 1: + return !!this.formGroup.get('requiredValue2')?.value; + default: + return false; + } + } + + protected readonly instance = inject(SkyModalInstance); + + constructor() { + this.formGroup = inject(FormBuilder).group({ + requiredValue1: undefined, + requiredValue2: undefined, + }); + } + + protected onCancelClick(): void { + this.instance.cancel(); + } + + protected onSaveClick(args: SkyProgressIndicatorActionClickArgs): void { + args.progressHandler.advance(); + this.instance.save(); + } + + protected updateIndex(changes: SkyProgressIndicatorChange): void { + this.activeIndex = changes.activeIndex; + } +} diff --git a/libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts b/libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts new file mode 100644 index 0000000000..700362d7a8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.spec.ts @@ -0,0 +1,64 @@ +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { CustomSkyHrefResolverService } from './custom-sky-href-resolver.service'; + +describe('CustomSkyHrefResolverService', () => { + let service: CustomSkyHrefResolverService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CustomSkyHrefResolverService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return a link as-is', async () => { + const url = 'https://www.blackbaud.com'; + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual(url); + expect(href.userHasAccess).toEqual(true); + }); + + it('should return a link with allow protocol', async () => { + const url = 'allow://www.blackbaud.com'; + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual('https://www.blackbaud.com'); + expect(href.userHasAccess).toEqual(true); + }); + + it('should return a link with deny protocol', async () => { + const url = 'deny://www.blackbaud.com'; + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual(url); + expect(href.userHasAccess).toEqual(false); + }); + + it('should return a link with slow protocol', fakeAsync(() => { + const url = 'slow://www.blackbaud.com'; + const result = service.resolveHref({ url }); + + result + .then((href) => { + expect(href.url).toEqual('https://www.blackbaud.com'); + expect(href.userHasAccess).toEqual(true); + }) + .catch(() => { + fail('expected test to resolve'); + }); + + tick(3000); + })); + + it('should return a link with unknown protocol', async () => { + const url = 'unknown://www.blackbaud.com'; + const href = await service.resolveHref({ url }); + + expect(href.url).toEqual(url); + expect(href.userHasAccess).toEqual(false); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts b/libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts new file mode 100644 index 0000000000..7de7212048 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/router/href/basic/custom-resolver/custom-sky-href-resolver.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; +import { SkyHref, SkyHrefResolver } from '@skyux/router'; + +/** + * Example of a custom resolver that returns a link as-is. Blackbaud uses a resolver to check + * whether a user has access to a link before returning it, and rewrites the link from a custom + * protocol to a standard protocol with a domain. + */ +@Injectable({ + providedIn: 'root', +}) +export class CustomSkyHrefResolverService implements SkyHrefResolver { + public resolveHref(param: { url: string }): Promise { + const url = param.url; + + if (url.startsWith('http:') || url.startsWith('https:')) { + return Promise.resolve({ + url, + userHasAccess: true, + }); + } + + if (url.startsWith('allow:')) { + return Promise.resolve({ + url: url.replace('allow:', 'https:'), + userHasAccess: true, + }); + } + + if (url.startsWith('deny:')) { + return Promise.resolve({ + url, + userHasAccess: false, + }); + } + + if (url.startsWith('slow:')) { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + url: url.replace('slow:', 'https:'), + userHasAccess: true, + }); + }, 3000); + }); + } + + if (url.startsWith('1bb-nav:')) { + return Promise.resolve({ + url: `https://docs.blackbaud.com/engineering-system-docs/learn/spa/spa-navigation/spa-to-spa-navigation`, + userHasAccess: true, + }); + } + + return Promise.resolve({ + url, + userHasAccess: false, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.css b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.css new file mode 100644 index 0000000000..f262a18cdc --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.css @@ -0,0 +1,16 @@ +:host { + display: block; +} + +span { + display: inline-block; + margin-right: var(--sky-margin-inline-xs); +} + +a[skyhref]::after { + display: block; + content: 'skyhref=“' attr(skyhref) '” ➜ href=“' attr(href) '”'; + font-size: 10px; + color: var(--sky-text-color-deemphasized); + text-decoration: none; +} diff --git a/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.html b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.html new file mode 100644 index 0000000000..b52f73e2b3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.html @@ -0,0 +1,25 @@ +

+ Link where the user has access: + Example.com +

+

+ Link where the user has access, translated: + Example.com with “allow” protocol +

+

+ Link where the user does not have access, hidden by default: + Example.com with “deny” protocol +

+

+ Link where the user does not have access, shown as plain text: + Example.com with “deny” protocol +

diff --git a/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.spec.ts new file mode 100644 index 0000000000..85a7ed411f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.spec.ts @@ -0,0 +1,83 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyHrefHarness, SkyHrefTestingModule } from '@skyux/router/testing'; + +import { RouterHrefBasicExampleComponent } from './example.component'; + +describe('SkyHrefRouterHrefBasicExampleComponent', () => { + let component: RouterHrefBasicExampleComponent; + let fixture: ComponentFixture; + + async function setup(options: { + userHasAccess: boolean; + dataSkyId: string; + }): Promise<{ + fixture: ComponentFixture; + loader: HarnessLoader; + hrefHarness: SkyHrefHarness; + }> { + TestBed.configureTestingModule({ + imports: [ + RouterHrefBasicExampleComponent, + SkyHrefTestingModule.with({ userHasAccess: options.userHasAccess }), + ], + }); + + fixture = TestBed.createComponent(RouterHrefBasicExampleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + await fixture.whenStable(); + + const loader = TestbedHarnessEnvironment.loader(fixture); + const hrefHarness = await loader.getHarness( + SkyHrefHarness.with({ dataSkyId: options.dataSkyId }), + ); + + return { fixture, loader, hrefHarness }; + } + + it('should create', async () => { + await setup({ userHasAccess: true, dataSkyId: 'my-href-allow' }); + expect(component).toBeTruthy(); + }); + + it('should show skyhref with access', async () => { + const { hrefHarness } = await setup({ + userHasAccess: true, + dataSkyId: 'my-href-allow', + }); + expect(hrefHarness).toBeTruthy(); + await expectAsync(hrefHarness.getHref()).toBeResolvedTo( + 'allow://example.com', + ); + await expectAsync(hrefHarness.getText()).toBeResolvedTo( + 'Example.com with “allow” protocol', + ); + await expectAsync(hrefHarness.isVisible()).toBeResolvedTo(true); + }); + + it('should hide skyhref without access', async () => { + const { hrefHarness } = await setup({ + userHasAccess: false, + dataSkyId: 'my-href-hidden', + }); + expect(hrefHarness).toBeTruthy(); + await expectAsync(hrefHarness.getHref()).toBeResolvedTo(null); + await expectAsync(hrefHarness.getText()).toBeResolvedTo(''); + await expectAsync(hrefHarness.isVisible()).toBeResolvedTo(false); + }); + + it('should unlink skyhref without access', async () => { + const { hrefHarness } = await setup({ + userHasAccess: false, + dataSkyId: 'my-href-unlinked', + }); + await expectAsync(hrefHarness.getHref()).toBeResolvedTo(null); + await expectAsync(hrefHarness.getText()).toBeResolvedTo( + 'Example.com with “deny” protocol', + ); + await expectAsync(hrefHarness.isVisible()).toBeResolvedTo(true); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.ts new file mode 100644 index 0000000000..93159b769f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/router/href/basic/example.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyHrefModule } from '@skyux/router'; + +@Component({ + selector: 'app-router-href-basic-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyHrefModule], +}) +export class RouterHrefBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.html b/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.html new file mode 100644 index 0000000000..175f57e953 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.html @@ -0,0 +1,77 @@ +
+ + + + @for (item of items; track item; let i = $index) { + + + {{ item.amount }}
+ {{ item.date }}
+ {{ item.vendor }} +
+
+ } +
+
+ + + +
+ + + + Receipt amount + + + {{ activeRecord.amount }} + + + + Date + + {{ activeRecord.date }} + + + + Vendor + + {{ activeRecord.vendor }} + + + + + Receipt image + + + {{ activeRecord.receiptImage }} + + + + + + + + + +
+
+ + + + + Approve expense + + + + Deny expense + + + + + +
+
+
diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.ts new file mode 100644 index 0000000000..131422e22d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/example.component.ts @@ -0,0 +1,188 @@ +import { Component, inject } from '@angular/core'; +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkySummaryActionBarModule } from '@skyux/action-bars'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyDescriptionListModule } from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyConfirmService, SkyConfirmType } from '@skyux/modals'; +import { + SkySplitViewMessage, + SkySplitViewMessageType, + SkySplitViewModule, +} from '@skyux/split-view'; + +import { Subject } from 'rxjs'; + +import { Record } from './record'; + +interface DemoForm { + approvedAmount: FormControl; + comments: FormControl; +} + +@Component({ + selector: 'app-split-view-basic-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyDescriptionListModule, + SkyInputBoxModule, + SkyRepeaterModule, + SkySplitViewModule, + SkySummaryActionBarModule, + ], +}) +export class SplitViewBasicExampleComponent { + protected set activeIndex(value: number) { + this.#_activeIndex = value; + this.activeRecord = this.items[this.#_activeIndex]; + this.#loadFormGroup(this.activeRecord); + } + + protected get activeIndex(): number { + return this.#_activeIndex; + } + + protected items = [ + { + id: 1, + amount: 73.19, + date: '5/13/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-13-19.png', + approvedAmount: 73.19, + comments: '', + }, + { + id: 2, + amount: 214.12, + date: '5/14/2020', + vendor: 'Office Max', + receiptImage: 'office-max-order.png', + approvedAmount: 214.12, + comments: '', + }, + { + id: 3, + amount: 29.99, + date: '5/14/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-14-19.png', + approvedAmount: 29.99, + comments: '', + }, + { + id: 4, + amount: 1500, + date: '5/15/2020', + vendor: 'Fresh Catering, LLC', + receiptImage: 'fresh-catering-llc-order.png', + approvedAmount: 1500, + comments: '', + }, + ]; + + protected activeRecord: Record; + protected splitViewDemoForm: FormGroup; + protected splitViewStream = new Subject(); + + #_activeIndex = 0; + + readonly #confirmSvc = inject(SkyConfirmService); + + constructor() { + // Start with the first item selected. + this.activeIndex = 0; + this.activeRecord = this.items[this.activeIndex]; + + this.splitViewDemoForm = new FormGroup({ + approvedAmount: new FormControl(this.activeRecord.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(this.activeRecord.comments, { + nonNullable: true, + }), + }); + } + + protected onItemClick(index: number): void { + // Prevent workspace from loading new data if the current workspace form is dirty. + if (this.splitViewDemoForm.dirty && index !== this.activeIndex) { + this.#openConfirmModal(index); + } else { + this.#loadWorkspace(index); + } + } + + protected onApprove(): void { + console.log('Approved clicked!'); + this.#saveForm(); + } + + protected onDeny(): void { + console.log('Denied clicked!'); + } + + #loadFormGroup(record: Record): void { + this.splitViewDemoForm = new FormGroup({ + approvedAmount: new FormControl(record.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(record.comments, { nonNullable: true }), + }); + } + + #loadWorkspace(index: number): void { + this.activeIndex = index; + this.#setFocusInWorkspace(); + } + + #openConfirmModal(index: number): void { + this.#confirmSvc + .open({ + message: + 'You have unsaved work. Would you like to save it before you change records?', + type: SkyConfirmType.Custom, + buttons: [ + { + action: 'yes', + text: 'Yes', + styleType: 'primary', + }, + { + action: 'discard', + text: 'Discard changes', + styleType: 'link', + }, + ], + }) + .closed.subscribe((closeArgs) => { + if (closeArgs.action.toLowerCase() === 'yes') { + this.#saveForm(); + } + + this.#loadWorkspace(index); + }); + } + + #saveForm(): void { + this.activeRecord.approvedAmount = + this.splitViewDemoForm.value.approvedAmount ?? 0; + this.activeRecord.comments = this.splitViewDemoForm.value.comments ?? ''; + + this.splitViewDemoForm.reset(this.splitViewDemoForm.value); + } + + #setFocusInWorkspace(): void { + const message: SkySplitViewMessage = { + type: SkySplitViewMessageType.FocusWorkspace, + }; + this.splitViewStream.next(message); + } +} diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/record.ts b/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/record.ts new file mode 100644 index 0000000000..0ea5c2dc3e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/basic/record.ts @@ -0,0 +1,9 @@ +export interface Record { + id: number; + amount: number; + date: string; + vendor: string; + receiptImage: string; + approvedAmount: number; + comments: string; +} diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.html b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.html new file mode 100644 index 0000000000..2285279706 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.html @@ -0,0 +1,106 @@ + +
+
+ + @if (unapprovedTransaction) { + + There is an unapproved transaction. + + } + SKY Developers, LLC + + Petty Cash Transactions + + + The transactions below cover various operating expenses which do not + fall under one of the budgets areas of expenditures. + + +
+
+ + + + @for (item of items; track item; let i = $index) { + + + {{ item.amount }}
+ {{ item.date }}
+ {{ item.vendor }} +
+
+ } +
+
+ + + +
+ + + + Receipt amount + + + {{ activeRecord.amount }} + + + + Date + + {{ activeRecord.date }} + + + + + Vendor + + + {{ activeRecord.vendor }} + + + + + Receipt image + + + {{ activeRecord.receiptImage }} + + + + + + + + + +
+
+ + + + + Approve expense + + + + Deny expense + + + + + +
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.scss b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.scss new file mode 100644 index 0000000000..5f9efd6248 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.scss @@ -0,0 +1,20 @@ +.page-flex { + display: flex; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.page-flex-header { + flex-grow: 0; +} + +.page-flex-main { + flex-grow: 1; + overflow: auto; + /* Required for the split view to fill this element instead of the document */ + position: relative; +} diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.spec.ts new file mode 100644 index 0000000000..1b024c7486 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.spec.ts @@ -0,0 +1,129 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyMediaQueryTestingController, + provideSkyMediaQueryTesting, +} from '@skyux/core/testing'; +import { SkyInputBoxHarness } from '@skyux/forms/testing'; +import { SkyRepeaterItemHarness } from '@skyux/lists/testing'; +import { SkySplitViewHarness } from '@skyux/split-view/testing'; + +import { SplitViewPageBoundExampleComponent } from './example.component'; + +describe('Split view example', () => { + async function setupTest(options: { dataSkyId?: string } = {}): Promise<{ + splitViewHarness: SkySplitViewHarness; + mediaQueryController: SkyMediaQueryTestingController; + fixture: ComponentFixture; + loader: HarnessLoader; + }> { + await TestBed.configureTestingModule({ + imports: [SplitViewPageBoundExampleComponent, NoopAnimationsModule], + providers: [provideSkyMediaQueryTesting()], + }).compileComponents(); + + const mediaQueryController = TestBed.inject(SkyMediaQueryTestingController); + + const fixture = TestBed.createComponent(SplitViewPageBoundExampleComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const splitViewHarness: SkySplitViewHarness = options.dataSkyId + ? await loader.getHarness( + SkySplitViewHarness.with({ dataSkyId: options.dataSkyId }), + ) + : await loader.getHarness(SkySplitViewHarness); + + return { splitViewHarness, mediaQueryController, fixture, loader }; + } + + it('should set up split view component and children', async () => { + const { splitViewHarness, fixture } = await setupTest(); + fixture.detectChanges(); + await fixture.whenStable(); + + // validate parent split view properties + await expectAsync(splitViewHarness.getDockType()).toBeResolvedTo('fill'); + await expectAsync(splitViewHarness.getDrawerIsVisible()).toBeResolvedTo( + true, + ); + await expectAsync(splitViewHarness.getWorkspaceIsVisible()).toBeResolvedTo( + true, + ); + + // query for drawer and workspace child components and validate properties + const drawerHarness = await splitViewHarness.getDrawer(); + const workspaceHarness = await splitViewHarness.getWorkspace(); + + await expectAsync(drawerHarness.getAriaLabel()).toBeResolvedTo( + 'Transaction list', + ); + await expectAsync(workspaceHarness.getAriaLabel()).toBeResolvedTo( + 'Transaction form', + ); + + // query for content and footer child components and their child elements + const contentHarness = await workspaceHarness.getContent(); + const footerHarness = await workspaceHarness.getFooter(); + + await expectAsync( + contentHarness?.queryHarnesses(SkyInputBoxHarness), + ).toBeResolved(); + await expectAsync( + footerHarness?.querySelector('sky-summary-action-bar-primary-action'), + ).toBeResolved(); + }); + + it('should switch between views in responsive mode', async () => { + const { splitViewHarness, mediaQueryController, fixture } = + await setupTest(); + + // set XS breakpoint to force split view into responsive mode + mediaQueryController.setBreakpoint('xs'); + + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(splitViewHarness.getDrawerIsVisible()).toBeResolvedTo( + false, + ); + await expectAsync(splitViewHarness.getWorkspaceIsVisible()).toBeResolvedTo( + true, + ); + await expectAsync(splitViewHarness.getBackButtonText()).toBeResolvedTo( + 'Back to list', + ); + + // switch to drawer view + await splitViewHarness.openDrawer(); + + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(splitViewHarness.getDrawerIsVisible()).toBeResolvedTo( + true, + ); + await expectAsync(splitViewHarness.getWorkspaceIsVisible()).toBeResolvedTo( + false, + ); + + const drawerHarness = await splitViewHarness.getDrawer(); + const drawerItems = await drawerHarness.queryHarnesses( + SkyRepeaterItemHarness, + ); + + // switch back to workspace view + await drawerItems[3].click(); + + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(splitViewHarness.getDrawerIsVisible()).toBeResolvedTo( + false, + ); + await expectAsync(splitViewHarness.getWorkspaceIsVisible()).toBeResolvedTo( + true, + ); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.ts b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.ts new file mode 100644 index 0000000000..9aabe73b19 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/example.component.ts @@ -0,0 +1,249 @@ +import { Component, inject } from '@angular/core'; +import { + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { SkySummaryActionBarModule } from '@skyux/action-bars'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyAlertModule } from '@skyux/indicators'; +import { + SkyDescriptionListModule, + SkyPageModule, + SkyPageSummaryModule, +} from '@skyux/layout'; +import { SkyRepeaterModule } from '@skyux/lists'; +import { SkyConfirmService, SkyConfirmType } from '@skyux/modals'; +import { + SkySplitViewMessage, + SkySplitViewMessageType, + SkySplitViewModule, +} from '@skyux/split-view'; + +import { Subject } from 'rxjs'; + +import { Record } from './record'; + +interface DemoForm { + approvedAmount: FormControl; + comments: FormControl; +} + +@Component({ + selector: 'app-split-view-page-bound-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.scss'], + imports: [ + FormsModule, + ReactiveFormsModule, + SkyAlertModule, + SkyDescriptionListModule, + SkyInputBoxModule, + SkyPageModule, + SkyPageSummaryModule, + SkyRepeaterModule, + SkySplitViewModule, + SkySummaryActionBarModule, + ], +}) +export class SplitViewPageBoundExampleComponent { + protected set activeIndex(value: number) { + this.#_activeIndex = value; + this.activeRecord = this.items[this.#_activeIndex]; + this.#loadFormGroup(this.activeRecord); + } + + protected get activeIndex(): number { + return this.#_activeIndex; + } + + protected items: Record[] = [ + { + id: 1, + amount: 73.19, + date: '5/13/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-13-19.png', + approvedAmount: 73.19, + comments: '', + }, + { + id: 2, + amount: 214.12, + date: '5/14/2020', + vendor: 'Office Max', + receiptImage: 'office-max-order.png', + approvedAmount: 214.12, + comments: '', + }, + { + id: 3, + amount: 29.99, + date: '5/14/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-14-19.png', + approvedAmount: 29.99, + comments: '', + }, + { + id: 4, + amount: 1500, + date: '5/15/2020', + vendor: 'Fresh Catering, LLC', + receiptImage: 'fresh-catering-llc-order.png', + approvedAmount: 1500, + comments: '', + }, + { + id: 5, + amount: 456.24, + date: '5/16/2020', + vendor: 'Wish', + receiptImage: 'wish-delivery-order.png', + approvedAmount: 456.24, + comments: '', + }, + { + id: 6, + amount: 62.37, + date: '5/16/2020', + vendor: 'Staples', + receiptImage: 'staples-paper-bulk-order.png', + approvedAmount: 62.37, + comments: '', + }, + { + id: 7, + amount: 51.84, + date: '5/17/2020', + vendor: 'amazon.com', + receiptImage: 'amzn-office-supply-order-5-17-19.png', + approvedAmount: 51.84, + comments: '', + }, + { + id: 8, + amount: 92.55, + date: '5/18/2020', + vendor: 'Home Depot', + receiptImage: 'home-depot-order.png', + approvedAmount: 0.0, + comments: '', + }, + { + id: 9, + amount: 38.29, + date: '5/18/2020', + vendor: 'Papa Johns', + receiptImage: 'papa-johns-order.png', + approvedAmount: 38.29, + comments: '', + }, + ]; + + protected activeRecord: Record; + protected splitViewDemoForm: FormGroup; + protected splitViewStream = new Subject(); + protected unapprovedTransaction = true; + + #_activeIndex = 0; + + readonly #confirmSvc = inject(SkyConfirmService); + + constructor() { + // Start with the first item selected. + this.activeIndex = 0; + this.activeRecord = this.items[this.activeIndex]; + + this.splitViewDemoForm = new FormGroup({ + approvedAmount: new FormControl(this.activeRecord.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(this.activeRecord.comments, { + nonNullable: true, + }), + }); + } + + protected onItemClick(index: number): void { + // Prevent workspace from loading new data if the current workspace form is dirty. + if (this.splitViewDemoForm.dirty && index !== this.activeIndex) { + this.#openConfirmModal(index); + } else { + this.#loadWorkspace(index); + } + } + + protected onApprove(): void { + console.log('Approved clicked!'); + this.#saveForm(); + } + + protected onDeny(): void { + console.log('Denied clicked!'); + } + + #loadFormGroup(record: Record): void { + this.splitViewDemoForm = new FormGroup({ + approvedAmount: new FormControl(record.approvedAmount, { + nonNullable: true, + }), + comments: new FormControl(record.comments, { nonNullable: true }), + }); + } + + #loadWorkspace(index: number): void { + this.activeIndex = index; + this.#setFocusInWorkspace(); + } + + #openConfirmModal(index: number): void { + this.#confirmSvc + .open({ + message: + 'You have unsaved work. Would you like to save it before you change records?', + type: SkyConfirmType.Custom, + buttons: [ + { + action: 'yes', + text: 'Yes', + styleType: 'primary', + }, + { + action: 'discard', + text: 'Discard changes', + styleType: 'link', + }, + ], + }) + .closed.subscribe((closeArgs) => { + if (closeArgs.action.toLowerCase() === 'yes') { + this.#saveForm(); + } + + this.#loadWorkspace(index); + }); + } + + #saveForm(): void { + this.activeRecord.approvedAmount = parseFloat( + `${this.splitViewDemoForm.value.approvedAmount ?? 0}`, + ); + + this.activeRecord.comments = this.splitViewDemoForm.value.comments ?? ''; + + this.unapprovedTransaction = + this.items.findIndex((item) => item.amount !== item.approvedAmount) >= 0; + + this.splitViewDemoForm.reset(this.splitViewDemoForm.value); + } + + #setFocusInWorkspace(): void { + const message: SkySplitViewMessage = { + type: SkySplitViewMessageType.FocusWorkspace, + }; + + this.splitViewStream.next(message); + } +} diff --git a/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/record.ts b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/record.ts new file mode 100644 index 0000000000..0ea5c2dc3e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/split-view/split-view/page-bound/record.ts @@ -0,0 +1,9 @@ +export interface Record { + id: number; + amount: number; + date: string; + vendor: string; + receiptImage: string; + approvedAmount: number; + comments: string; +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/address-form.component.ts b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/address-form.component.ts new file mode 100644 index 0000000000..fdc78b35e2 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/address-form.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-address-form', + template: `
Address form
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AddressFormComponent {} diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.html new file mode 100644 index 0000000000..d8bfd46996 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.html @@ -0,0 +1,3 @@ + diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.ts new file mode 100644 index 0000000000..c024dce291 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/example.component.ts @@ -0,0 +1,28 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyModalCloseArgs, SkyModalService } from '@skyux/modals'; + +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-tabs-sectioned-form-modal-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TabsSectionedFormModalExampleComponent { + readonly #modalSvc = inject(SkyModalService); + + protected openModal(): void { + const modalInstance = this.#modalSvc.open(ModalComponent, { + size: 'large', + }); + + modalInstance.closed.subscribe((result: SkyModalCloseArgs) => { + if (result.reason === 'cancel') { + console.log(`Modal cancelled`); + } else if (result.reason === 'save') { + console.log(`Modal saved`); + } + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.html b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.html new file mode 100644 index 0000000000..62525e508a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.html @@ -0,0 +1,26 @@ +
+ + + + + + + + + + + + @if (formGroup.get('id')?.invalid) { + + Enter a valid value. + + } + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.ts b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.ts new file mode 100644 index 0000000000..25f779b308 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/information-form.component.ts @@ -0,0 +1,81 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnInit, + inject, +} from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyStatusIndicatorModule } from '@skyux/indicators'; +import { SkyFluidGridModule } from '@skyux/layout'; +import { SkySectionedFormService } from '@skyux/tabs'; + +@Component({ + selector: 'app-information-form', + templateUrl: './information-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ReactiveFormsModule, + SkyInputBoxModule, + SkyFluidGridModule, + SkyStatusIndicatorModule, + ], +}) +export class InformationFormComponent implements OnInit { + protected id = '5324901'; + protected formGroup: FormGroup<{ + name: FormControl; + nameRequired: FormControl; + id: FormControl; + }>; + protected name = ''; + protected nameRequired = false; + + readonly #sectionedFormSvc = inject(SkySectionedFormService); + readonly #changeDetector = inject(ChangeDetectorRef); + + constructor() { + this.formGroup = inject(FormBuilder).group({ + name: new FormControl(this.name, { nonNullable: true }), + nameRequired: new FormControl(this.nameRequired, { nonNullable: true }), + id: new FormControl(this.id, { + nonNullable: true, + validators: [Validators.pattern('^[0-9]+$')], + }), + }); + } + + public ngOnInit(): void { + this.formGroup.valueChanges.subscribe((changes) => { + this.id = changes.id ?? ''; + this.name = changes.name ?? ''; + this.nameRequired = !!changes.nameRequired; + this.#checkValidity(); + }); + + this.#changeDetector.markForCheck(); + } + + #checkValidity(): void { + if (this.nameRequired) { + this.formGroup.get('name')?.setValidators([Validators.required]); + this.#sectionedFormSvc.requiredFieldChanged(true); + } else { + this.formGroup.get('name')?.setValidators([]); + this.#sectionedFormSvc.requiredFieldChanged(false); + } + + if (!this.formGroup.get('name')?.value && this.nameRequired) { + this.#sectionedFormSvc.invalidFieldChanged(true); + } else { + this.#sectionedFormSvc.invalidFieldChanged(false); + } + } +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.html b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.html new file mode 100644 index 0000000000..c0f6d238a1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + @if (tabsHidden()) { + + } + + + + diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.ts b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.ts new file mode 100644 index 0000000000..29f9464e22 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/modal.component.ts @@ -0,0 +1,51 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ViewChild, + inject, +} from '@angular/core'; +import { SkyIconModule } from '@skyux/icon'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { SkySectionedFormComponent, SkySectionedFormModule } from '@skyux/tabs'; + +import { AddressFormComponent } from './address-form.component'; +import { InformationFormComponent } from './information-form.component'; +import { PhoneFormComponent } from './phone-form.component'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AddressFormComponent, + InformationFormComponent, + PhoneFormComponent, + SkyIconModule, + SkyModalModule, + SkySectionedFormModule, + ], +}) +export class ModalComponent { + @ViewChild(SkySectionedFormComponent) + protected sectionedFormComponent: SkySectionedFormComponent | undefined; + + protected activeIndexDisplay: number | undefined; + protected activeTab = true; + + protected readonly modalInstance = inject(SkyModalInstance); + readonly #changeDetector = inject(ChangeDetectorRef); + + protected onIndexChanged(newIndex: number): void { + this.activeIndexDisplay = newIndex; + this.#changeDetector.markForCheck(); + } + + protected tabsHidden(): boolean { + return !this.sectionedFormComponent?.tabsVisible(); + } + + protected showTabs(): void { + this.sectionedFormComponent?.showTabs(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/phone-form.component.ts b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/phone-form.component.ts new file mode 100644 index 0000000000..a9a3b4b009 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/sectioned-form/modal/phone-form.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-phone-form', + template: `
Phone form
`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PhoneFormComponent {} diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.html new file mode 100644 index 0000000000..cd0e0da25f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.html @@ -0,0 +1,7 @@ + + @for (tab of tabArray; track tab; let i = $index) { + + {{ tab.tabContent }} + + } + diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.ts new file mode 100644 index 0000000000..23f4493f09 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic-add-close/example.component.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; +import { SkyTabsModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-tabs-dynamic-add-close-example', + templateUrl: './example.component.html', + imports: [SkyTabsModule], +}) +export class TabsDynamicAddCloseExampleComponent { + protected tabArray = [ + { + tabHeading: 'Tab 1', + tabContent: 'Content for Tab 1', + }, + { + tabHeading: 'Tab 2', + tabContent: 'Content for Tab 2', + }, + { + tabHeading: 'Tab 3', + tabContent: 'Content for Tab 3', + }, + ]; + + #tabCounter = 3; + + protected onNewTabClick(): void { + this.#tabCounter++; + + this.tabArray.push({ + tabHeading: 'Tab ' + this.#tabCounter, + tabContent: 'Content for Tab' + this.#tabCounter, + }); + } + + protected onCloseClick(arrayIndex: number): void { + this.tabArray.splice(arrayIndex, 1); + } +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.html new file mode 100644 index 0000000000..9871b98702 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.html @@ -0,0 +1,7 @@ + + @for (tab of tabArray; track tab) { + + {{ tab.tabContent }} + + } + diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.ts new file mode 100644 index 0000000000..41ce91a3a7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/dynamic/example.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { SkyTabsModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-tabs-dynamic-example', + templateUrl: './example.component.html', + imports: [SkyTabsModule], +}) +export class TabsDynamicExampleComponent { + protected tabArray = [ + { + tabHeading: 'Tab 1', + tabContent: 'A list containing 25012 items', + }, + { + tabHeading: 'Tab 2', + tabContent: 'A list containing 280 items', + }, + { + tabHeading: 'Tab 3', + tabContent: "This tab doesn't have a list of items", + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.html new file mode 100644 index 0000000000..7e2670f920 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.html @@ -0,0 +1,9 @@ + + Content for Tab 1 + Content for Tab 2 + @if (showTab3) { + + Content for Tab 3 + + } + diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.ts new file mode 100644 index 0000000000..6df3a3adac --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/static-add-close/example.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { SkyTabsModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-tabs-static-add-close-example', + templateUrl: './example.component.html', + imports: [SkyTabsModule], +}) +export class TabsStaticAddCloseExampleComponent { + protected showTab3 = true; + + protected onNewTabClick(): void { + alert('Add tab clicked!'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.html new file mode 100644 index 0000000000..292056ef2b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.html @@ -0,0 +1,5 @@ + + Content for Tab 1 + Content for Tab 2 + Content for Tab 3 + diff --git a/libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.ts new file mode 100644 index 0000000000..b34da31b86 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/tabs/static/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyTabsModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-tabs-static-example', + templateUrl: './example.component.html', + imports: [SkyTabsModule], +}) +export class TabsStaticExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.html new file mode 100644 index 0000000000..1500f94a1f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.html @@ -0,0 +1,7 @@ + + Tab 1 content + + Tab 2 content + + Tab 3 content + diff --git a/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.ts new file mode 100644 index 0000000000..b8e4c925a0 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/basic/example.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { SkyVerticalTabsetModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-tabs-vertical-tabs-basic-example', + templateUrl: './example.component.html', + imports: [SkyVerticalTabsetModule], +}) +export class TabsVerticalTabsBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.html new file mode 100644 index 0000000000..c51d591a62 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.html @@ -0,0 +1,20 @@ + + @for (group of groups; track group) { + + @for (tab of group.subTabs; track tab) { + + {{ tab.content }} + + } + + } + diff --git a/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.ts new file mode 100644 index 0000000000..2c202b6169 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/example.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { SkyVerticalTabsetModule } from '@skyux/tabs'; + +import { TabGroup } from './group'; + +@Component({ + selector: 'app-tabs-vertical-tabs-grouped-example', + templateUrl: './example.component.html', + imports: [SkyVerticalTabsetModule], +}) +export class TabsVerticalTabsGroupedExampleComponent { + protected groups: TabGroup[] = [ + { + heading: 'Group 1', + isOpen: false, + isDisabled: false, + subTabs: [ + { tabHeading: 'Group 1 — Tab 1', content: 'Group 1 — Tab 1 Content' }, + { + tabHeading: 'Group 1 — Tab 2', + content: 'Group 1 — Tab 2 Content', + tabHeaderCount: 7, + }, + ], + }, + { + heading: 'Group 2', + isOpen: true, + isDisabled: false, + subTabs: [ + { + tabHeading: 'Group 2 — Tab 1', + content: 'Group 2 — Tab 1 Content', + active: true, + }, + { + tabHeading: 'Group 2 — Tab 2 — Disabled', + content: 'Group 2 — Tab 2 Content', + disabled: true, + }, + ], + }, + { + heading: 'Disabled', + isOpen: false, + isDisabled: true, + subTabs: [], + }, + ]; +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/group.ts b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/group.ts new file mode 100644 index 0000000000..4b6cbd178f --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/vertical-tabs/grouped/group.ts @@ -0,0 +1,12 @@ +export interface TabGroup { + heading: string; + isOpen: boolean; + isDisabled: boolean; + subTabs: { + tabHeading: string; + content: string; + tabHeaderCount?: number; + active?: boolean; + disabled?: boolean; + }[]; +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.html b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.html new file mode 100644 index 0000000000..2f1f4e35c9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.html @@ -0,0 +1,3 @@ + diff --git a/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.ts new file mode 100644 index 0000000000..62718c38da --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/example.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from '@angular/core'; +import { SkyModalService } from '@skyux/modals'; + +import { ModalComponent } from './modal.component'; + +@Component({ + standalone: true, + selector: 'app-tabs-wizard-basic-example', + templateUrl: './example.component.html', +}) +export class TabsWizardBasicExampleComponent { + readonly #modalSvc = inject(SkyModalService); + + #modalSize = 'large'; + + protected openWizard(): void { + this.#modalSvc.open(ModalComponent, { size: this.#modalSize }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.html b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.html new file mode 100644 index 0000000000..e6c50846c8 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.html @@ -0,0 +1,90 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.ts b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.ts new file mode 100644 index 0000000000..7190bac8f9 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tabs/wizard/basic/modal.component.ts @@ -0,0 +1,78 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyCheckboxModule, SkyInputBoxModule } from '@skyux/forms'; +import { SkyFluidGridModule } from '@skyux/layout'; +import { SkyModalInstance, SkyModalModule } from '@skyux/modals'; +import { SkyPhoneFieldModule } from '@skyux/phone-field'; +import { SkyTabIndex, SkyTabsModule } from '@skyux/tabs'; + +@Component({ + selector: 'app-modal', + templateUrl: './modal.component.html', + imports: [ + ReactiveFormsModule, + SkyCheckboxModule, + SkyFluidGridModule, + SkyInputBoxModule, + SkyModalModule, + SkyPhoneFieldModule, + SkyTabsModule, + ], +}) +export class ModalComponent implements OnInit { + protected activeIndex: SkyTabIndex = 0; + protected formGroup: FormGroup; + protected isSaveDisabled = true; + protected isStep2Disabled = true; + protected isStep3Disabled = true; + protected title = 'New Member Sign-up'; + + readonly #instance = inject(SkyModalInstance); + + constructor() { + this.formGroup = inject(FormBuilder).group({ + firstName: new FormControl('', Validators.required), + middleName: new FormControl(''), + lastName: new FormControl('', Validators.required), + phoneNumber: new FormControl('', Validators.required), + email: new FormControl('', Validators.required), + termsAccepted: new FormControl(false), + mailingList: new FormControl(false), + }); + } + + public ngOnInit(): void { + this.formGroup.valueChanges.subscribe(() => { + this.#checkRequirementsMet(); + }); + } + + protected onCancelClick(): void { + this.#instance.cancel(); + } + + protected onSave(): void { + this.#instance.save(); + } + + #checkRequirementsMet(): void { + this.isStep2Disabled = !( + this.formGroup?.get('firstName')?.value && + this.formGroup?.get('lastName')?.value + ); + + this.isStep3Disabled = !( + this.formGroup?.get('phoneNumber')?.value && + this.formGroup?.get('phoneNumber')?.valid && + this.formGroup?.get('email')?.value + ); + + this.isSaveDisabled = !this.formGroup?.get('termsAccepted')?.value; + } +} diff --git a/libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.html b/libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.html new file mode 100644 index 0000000000..f78929fbe7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.html @@ -0,0 +1,17 @@ +
+
+ + @if (myText.errors?.['companyName']) { + + } + +
+
diff --git a/libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.ts b/libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.ts new file mode 100644 index 0000000000..5ef54ac69b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/text-editor/help-key/example.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyTextEditorModule } from '@skyux/text-editor'; + +function validateText( + control: AbstractControl, +): ValidationErrors | null { + return !control.value?.includes('Blackbaud') ? { companyName: true } : null; +} + +@Component({ + selector: 'app-text-editor-help-key-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyTextEditorModule], +}) +export class TextEditorHelpKeyExampleComponent { + protected formGroup: FormGroup; + public myText: FormControl; + + #richText = `Exclusively committed to your impact

Since day one, Blackbaud has been 100% focused on driving impact for social good organizations.

We equip change agents with cloud software, services, expertise, and data intelligence designed with unmatched insight and supported with unparalleled commitment. Every day, our customers achieve unmatched impact as they advance their missions.

`; + + constructor() { + this.myText = new FormControl(this.#richText, { + nonNullable: true, + validators: [Validators.required, validateText], + }); + + this.formGroup = inject(FormBuilder).group({ + myText: this.myText, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.html b/libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.html new file mode 100644 index 0000000000..2b8e321239 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.html @@ -0,0 +1 @@ + diff --git a/libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.ts b/libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.ts new file mode 100644 index 0000000000..35f291a92d --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/text-editor/rich-text-display/example.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SkyRichTextDisplayModule } from '@skyux/text-editor'; + +@Component({ + selector: 'app-text-editor-rich-text-display-example', + templateUrl: './example.component.html', + imports: [SkyRichTextDisplayModule], +}) +export class TextEditorRichTextDisplayExampleComponent { + protected richText = `Exclusively committed to your impact

Since day one, Blackbaud has been 100% focused on driving impact for social good organizations.

We equip change agents with cloud software, services, expertise, and data intelligence designed with unmatched insight and supported with unparalleled commitment. Every day, our customers achieve unmatched impact as they advance their missions.

`; +} diff --git a/libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.html b/libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.html new file mode 100644 index 0000000000..e1b0f2535e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.html @@ -0,0 +1,20 @@ +
+
+ + @if (myText.errors?.['companyName']) { + + } + +
+
diff --git a/libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.ts b/libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.ts new file mode 100644 index 0000000000..a120a8d4a5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/text-editor/text-editor/example.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { SkyTextEditorModule } from '@skyux/text-editor'; + +function validateText( + control: AbstractControl, +): ValidationErrors | null { + return !control.value?.includes('Blackbaud') ? { companyName: true } : null; +} + +@Component({ + selector: 'app-text-editor-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyTextEditorModule], +}) +export class TextEditorExampleComponent { + protected formGroup: FormGroup; + public myText: FormControl; + + #richText = `Exclusively committed to your impact

Since day one, Blackbaud has been 100% focused on driving impact for social good organizations.

We equip change agents with cloud software, services, expertise, and data intelligence designed with unmatched insight and supported with unparalleled commitment. Every day, our customers achieve unmatched impact as they advance their missions.

`; + + constructor() { + this.myText = new FormControl(this.#richText, { + nonNullable: true, + validators: [Validators.required, validateText], + }); + + this.formGroup = inject(FormBuilder).group({ + myText: this.myText, + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/theme/box/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/theme/box/basic/example.component.ts new file mode 100644 index 0000000000..a0a8919200 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/theme/box/basic/example.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-theme-box-basic-example', + template: ` +
+ +
+ `, +}) +export class ThemeBoxBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/theme/status-indicator/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/theme/status-indicator/basic/example.component.ts new file mode 100644 index 0000000000..0d048527c1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/theme/status-indicator/basic/example.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-theme-status-indicator-basic-example', + template: ` +
Danger status indicator
+
Info status indicator
+
Success status indicator
+
Warning status indicator
+ `, +}) +export class ThemeStatusIndicatorBasicExampleComponent {} diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.html b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.html new file mode 100644 index 0000000000..f08bff1a47 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.html @@ -0,0 +1,4 @@ + diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.spec.ts new file mode 100644 index 0000000000..99cc1144c7 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.spec.ts @@ -0,0 +1,81 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyMediaQueryTestingController, + provideSkyMediaQueryTesting, +} from '@skyux/core/testing'; +import { SkyTileDashboardHarness } from '@skyux/tiles/testing'; + +import { TilesBasicExampleComponent } from './example.component'; + +describe('Tile dashboard example', () => { + async function setupTest( + options: { + dataSkyId?: string; + } = {}, + ): Promise<{ + tileDashboardHarness: SkyTileDashboardHarness; + mediaQueryController: SkyMediaQueryTestingController; + fixture: ComponentFixture; + }> { + await TestBed.configureTestingModule({ + imports: [TilesBasicExampleComponent, NoopAnimationsModule], + providers: [provideSkyMediaQueryTesting()], + }).compileComponents(); + + const mediaQueryController = TestBed.inject(SkyMediaQueryTestingController); + + const fixture = TestBed.createComponent(TilesBasicExampleComponent); + const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); + + const tileDashboardHarness: SkyTileDashboardHarness = options.dataSkyId + ? await loader.getHarness( + SkyTileDashboardHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyTileDashboardHarness); + + return { tileDashboardHarness, mediaQueryController, fixture }; + } + + it('should set up the tile dashboard', async () => { + const { tileDashboardHarness, mediaQueryController, fixture } = + await setupTest(); + + mediaQueryController.setBreakpoint('lg'); + + await expectAsync(tileDashboardHarness.isMultiColumn()).toBeResolvedTo( + true, + ); + + const tileHarness = await tileDashboardHarness.getTile({ + dataSkyId: 'tile-1', + }); + + await expectAsync(tileHarness.isExpanded()).toBeResolvedTo(false); + await tileHarness.expand(); + await expectAsync(tileHarness.isExpanded()).toBeResolvedTo(true); + + const tile1Component = fixture.debugElement.query(By.css('div.tile1')); + const settingsSpy = spyOn( + tile1Component.componentInstance, + 'tileSettingsClick', + ); + await tileHarness.clickSettingsButton(); + + expect(settingsSpy).toHaveBeenCalled(); + + const tileContentHarness = await tileHarness.getContent(); + + const tileContentSectionHarness = await tileContentHarness.getSection({ + dataSkyId: 'section-1', + }); + + await expectAsync( + (await tileContentSectionHarness.host()).text(), + ).toBeResolvedTo('Section 1'); + }); +}); diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.ts new file mode 100644 index 0000000000..63f9e2dcb5 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/example.component.ts @@ -0,0 +1,57 @@ +import { Component } from '@angular/core'; +import { SkyTileDashboardConfig, SkyTilesModule } from '@skyux/tiles'; + +import { Tile1Component } from './tile1.component'; +import { Tile2Component } from './tile2.component'; + +@Component({ + selector: 'app-tiles-basic-example', + templateUrl: './example.component.html', + imports: [SkyTilesModule], +}) +export class TilesBasicExampleComponent { + protected dashboardConfig: SkyTileDashboardConfig = { + tiles: [ + { + id: 'tile1', + componentType: Tile1Component, + }, + { + id: 'tile2', + componentType: Tile2Component, + }, + ], + layout: { + singleColumn: { + tiles: [ + { + id: 'tile2', + isCollapsed: false, + }, + { + id: 'tile1', + isCollapsed: true, + }, + ], + }, + multiColumn: [ + { + tiles: [ + { + id: 'tile1', + isCollapsed: true, + }, + ], + }, + { + tiles: [ + { + id: 'tile2', + isCollapsed: false, + }, + ], + }, + ], + }, + }; +} diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.html b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.html new file mode 100644 index 0000000000..a07c5cc59a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.html @@ -0,0 +1,17 @@ + + Tile 1 + $123.4m + + + Section 1 + + Section 2 + Section 3 + + diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.ts b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.ts new file mode 100644 index 0000000000..221b43897a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile1.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { SkyTilesModule } from '@skyux/tiles'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'div.tile1', + templateUrl: './tile1.component.html', + imports: [SkyTilesModule], +}) +export class Tile1Component { + public tileSettingsClick(): void { + alert('tile settings clicked'); + } +} diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.html b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.html new file mode 100644 index 0000000000..cc6dbab009 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.html @@ -0,0 +1,8 @@ + + Tile 2 + + Section 1 + Section 2 + Section 3 + + diff --git a/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.ts b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.ts new file mode 100644 index 0000000000..f2f9de2245 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/tiles/tiles/basic/tile2.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { SkyTilesModule } from '@skyux/tiles'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'div.tile2', + templateUrl: './tile2.component.html', + imports: [SkyTilesModule], +}) +export class Tile2Component {} diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.html b/libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.html new file mode 100644 index 0000000000..557671fa6b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.html @@ -0,0 +1,10 @@ + + diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.ts new file mode 100644 index 0000000000..56acc5bb34 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/basic/example.component.ts @@ -0,0 +1,21 @@ +import { Component, inject } from '@angular/core'; +import { SkyToastService, SkyToastType } from '@skyux/toast'; + +@Component({ + standalone: true, + selector: 'app-toast-basic-example', + templateUrl: './example.component.html', +}) +export class ToastBasicExampleComponent { + readonly #toastSvc = inject(SkyToastService); + + protected openToast(): void { + this.#toastSvc.openMessage('This is a sample toast message.', { + type: SkyToastType.Success, + }); + } + + protected closeAll(): void { + this.#toastSvc.closeAll(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-context.ts b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-context.ts new file mode 100644 index 0000000000..09f24b97b1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-context.ts @@ -0,0 +1,3 @@ +export class CustomToastContext { + constructor(public message: string) {} +} diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.html b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.html new file mode 100644 index 0000000000..79ac497b52 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.html @@ -0,0 +1 @@ +
Custom message: {{ context.message }}
diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.ts b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.ts new file mode 100644 index 0000000000..733e74f186 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/custom-toast.component.ts @@ -0,0 +1,18 @@ +import { Component, inject } from '@angular/core'; +import { SkyToastInstance } from '@skyux/toast'; + +import { CustomToastContext } from './custom-context'; + +@Component({ + standalone: true, + selector: 'app-toast-content-example', + templateUrl: './custom-toast.component.html', +}) +export class CustomToastComponent { + protected readonly context = inject(CustomToastContext); + readonly #instance = inject(SkyToastInstance); + + protected close(): void { + this.#instance.close(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.html b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.html new file mode 100644 index 0000000000..557671fa6b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.html @@ -0,0 +1,10 @@ + + diff --git a/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.ts b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.ts new file mode 100644 index 0000000000..a9afc90841 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/toast/toast/custom-component/example.component.ts @@ -0,0 +1,37 @@ +import { Component, inject } from '@angular/core'; +import { SkyToastService, SkyToastType } from '@skyux/toast'; + +import { CustomToastContext } from './custom-context'; +import { CustomToastComponent } from './custom-toast.component'; + +@Component({ + standalone: true, + selector: 'app-toast-custom-component-example', + templateUrl: './example.component.html', +}) +export class ToastCustomComponentExampleComponent { + readonly #toastSvc = inject(SkyToastService); + + protected openToast(): void { + const context = new CustomToastContext( + 'This toast has embedded a custom component for its content.', + ); + + this.#toastSvc.openComponent( + CustomToastComponent, + { + type: SkyToastType.Success, + }, + [ + { + provide: CustomToastContext, + useValue: context, + }, + ], + ); + } + + protected closeAll(): void { + this.#toastSvc.closeAll(); + } +} diff --git a/libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.html b/libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.html new file mode 100644 index 0000000000..38ce1106c1 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.html @@ -0,0 +1,7 @@ +
+
+ + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.ts b/libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.ts new file mode 100644 index 0000000000..244445dcb3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/email-validation/control-validator/example.component.ts @@ -0,0 +1,34 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyValidators } from '@skyux/validation'; + +@Component({ + selector: 'app-validation-email-validation-control-validator-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyInputBoxModule], +}) +export class ValidationEmailValidationControlValidatorExampleComponent { + protected get emailControl(): AbstractControl | null { + return this.formGroup.get('email'); + } + + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + email: new FormControl(undefined, [ + Validators.required, + SkyValidators.email, + ]), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.html b/libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.html new file mode 100644 index 0000000000..8a7be81bd4 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.html @@ -0,0 +1,13 @@ +
+
+ + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.ts b/libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.ts new file mode 100644 index 0000000000..1d2da25990 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/email-validation/directive/example.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyEmailValidationModule } from '@skyux/validation'; + +@Component({ + selector: 'app-validation-email-validation-directive-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyEmailValidationModule, + SkyInputBoxModule, + ], +}) +export class ValidationEmailValidationDirectiveExampleComponent { + protected exampleModel: { + emailAddress?: string; + } = {}; +} diff --git a/libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.html b/libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.html new file mode 100644 index 0000000000..a3c2a1943b --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.html @@ -0,0 +1,7 @@ +
+
+ + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.ts b/libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.ts new file mode 100644 index 0000000000..5364ec4da3 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/url-validation/control-validator/example.component.ts @@ -0,0 +1,36 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { SkyValidators } from '@skyux/validation'; + +@Component({ + selector: 'app-validation-url-validation-control-validator-example', + templateUrl: './example.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyInputBoxModule], +}) +export class ValidationUrlValidationControlValidatorExampleComponent { + protected get urlControl(): AbstractControl | null { + return this.formGroup.get('url'); + } + + protected formGroup: FormGroup; + + constructor() { + this.formGroup = inject(FormBuilder).group({ + url: new FormControl(undefined, [ + Validators.required, + SkyValidators.url({ + rulesetVersion: 2, + }), + ]), + }); + } +} diff --git a/libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.html b/libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.html new file mode 100644 index 0000000000..90dc2d446c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.html @@ -0,0 +1,11 @@ +
+
+ + + +
+
diff --git a/libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.ts b/libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.ts new file mode 100644 index 0000000000..1922e58728 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/validation/url-validation/directive/example.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { + SkyUrlValidationModule, + SkyUrlValidationOptions, +} from '@skyux/validation'; + +@Component({ + selector: 'app-validation-url-validation-directive-example', + templateUrl: './example.component.html', + imports: [ + FormsModule, + ReactiveFormsModule, + SkyInputBoxModule, + SkyUrlValidationModule, + ], +}) +export class ValidationUrlValidationDirectiveExampleComponent { + protected exampleModel: { + url?: string; + } = {}; + + protected skyUrlValidationOptions: SkyUrlValidationOptions = { + rulesetVersion: 2, + }; +} diff --git a/libs/components/code-examples/tsconfig.json b/libs/components/code-examples/tsconfig.json new file mode 100644 index 0000000000..c63981277b --- /dev/null +++ b/libs/components/code-examples/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.lib.prod.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/components/code-examples/tsconfig.lib.json b/libs/components/code-examples/tsconfig.lib.json new file mode 100644 index 0000000000..4952dcda5f --- /dev/null +++ b/libs/components/code-examples/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true + }, + "exclude": ["src/test.ts", "**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/components/code-examples/tsconfig.lib.prod.json b/libs/components/code-examples/tsconfig.lib.prod.json new file mode 100644 index 0000000000..2a2faa884c --- /dev/null +++ b/libs/components/code-examples/tsconfig.lib.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/libs/components/code-examples/tsconfig.spec.json b/libs/components/code-examples/tsconfig.spec.json new file mode 100644 index 0000000000..bb6f4b9ec9 --- /dev/null +++ b/libs/components/code-examples/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["jasmine", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json index 11946aeeff..8a473e9760 100644 --- a/libs/components/packages/package.json +++ b/libs/components/packages/package.json @@ -48,6 +48,7 @@ "@skyux/assets": "0.0.0-PLACEHOLDER", "@skyux/autonumeric": "0.0.0-PLACEHOLDER", "@skyux/avatar": "0.0.0-PLACEHOLDER", + "@skyux/code-examples": "0.0.0-PLACEHOLDER", "@skyux/colorpicker": "0.0.0-PLACEHOLDER", "@skyux/config": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", diff --git a/libs/components/tiles/project.json b/libs/components/tiles/project.json index a61af1d2d1..0f9f4c2b0a 100644 --- a/libs/components/tiles/project.json +++ b/libs/components/tiles/project.json @@ -24,7 +24,7 @@ "dependsOn": [ "^build", { - "projects": ["testing"], + "projects": ["core", "help-inline", "indicators"], "target": "build" } ], diff --git a/tsconfig.base.json b/tsconfig.base.json index dfd729305b..f000dd6f2d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -51,6 +51,7 @@ "@skyux/avatar/testing": [ "libs/components/avatar/testing/src/public-api.ts" ], + "@skyux/code-examples": ["libs/components/code-examples/src/index.ts"], "@skyux/colorpicker": ["libs/components/colorpicker/src/index.ts"], "@skyux/colorpicker/testing": [ "libs/components/colorpicker/testing/src/public-api.ts" From c387ac21cc0f6e0bd1a1603ab083dc8de1b105c2 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Mon, 3 Feb 2025 10:58:46 -0500 Subject: [PATCH 2/2] fix(components/ag-grid): refocus cell editors on api refresh (#3087) (#3092) :cherries: Cherry picked from #3087 [fix(components/ag-grid): refocus cell editors on api refresh](https://github.com/blackbaud/skyux/pull/3087) [AB#3240868](https://dev.azure.com/blackbaud/f565481a-7bc9-4083-95d5-4f953da6d499/_workitems/edit/3240868) Co-authored-by: John White <750350+johnhwhite@users.noreply.github.com> --- ...cell-editor-autocomplete.component.spec.ts | 36 ++++++++++--- .../cell-editor-autocomplete.component.ts | 15 ++++-- .../cell-editor-currency.component.spec.ts | 30 +++++++++-- .../cell-editor-currency.component.ts | 10 ++++ .../cell-editor-lookup.component.spec.ts | 51 ++++++++++++++----- .../cell-editor-lookup.component.ts | 15 ++++-- .../cell-editor-number.component.spec.ts | 41 +++++++++++++-- .../cell-editor-number.component.ts | 28 ++++++++-- .../cell-editor-text.component.spec.ts | 24 +++++++++ .../cell-editor-text.component.ts | 13 +++++ 10 files changed, 223 insertions(+), 40 deletions(-) diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.spec.ts index 3cba2a3446..7aa6a4989d 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.spec.ts @@ -51,19 +51,21 @@ describe('SkyCellEditorAutocompleteComponent', () => { }); describe('agInit', () => { - const api = jasmine.createSpyObj('api', [ - 'getDisplayNameForColumn', - 'getGridOption', - 'stopEditing', - ]); - api.getGridOption.and.returnValue(true); + let api: jasmine.SpyObj; let cellEditorParams: Partial; let column: AgColumn; + let gridCell: HTMLDivElement; const selection = data[0]; const rowNode = new RowNode({} as BeanCollection); rowNode.rowHeight = 37; beforeEach(() => { + api = jasmine.createSpyObj('api', [ + 'getDisplayNameForColumn', + 'getGridOption', + 'stopEditing', + ]); + api.getGridOption.and.returnValue(true); column = new AgColumn( { colId: 'col', @@ -72,11 +74,13 @@ describe('SkyCellEditorAutocompleteComponent', () => { 'col', true, ); + gridCell = document.createElement('div'); cellEditorParams = { api, value: selection, column, + eGridCell: gridCell, node: rowNode, colDef: {}, cellStartedEdit: true, @@ -95,14 +99,30 @@ describe('SkyCellEditorAutocompleteComponent', () => { tick(); component.onAutocompleteOpenChange(true); - component.onBlur(); + component.onBlur({} as FocusEvent); expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled(); component.onAutocompleteOpenChange(false); - component.onBlur(); + component.onBlur({} as FocusEvent); expect(cellEditorParams.api?.stopEditing).toHaveBeenCalled(); })); + it('should respond to refocus', fakeAsync(() => { + fixture.detectChanges(); + + const input = nativeElement.querySelector('input') as HTMLInputElement; + spyOn(input, 'focus'); + + component.agInit(cellEditorParams as SkyCellEditorAutocompleteParams); + component.onBlur({ + relatedTarget: gridCell, + } as unknown as FocusEvent); + tick(); + expect(input).toBeVisible(); + expect(input.focus).toHaveBeenCalled(); + expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled(); + })); + it('should set the correct aria label', () => { api.getDisplayNameForColumn.and.returnValue('Testing'); component.agInit({ diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.ts index 0564d7ada3..e316af93d0 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-autocomplete/cell-editor-autocomplete.component.ts @@ -43,9 +43,18 @@ export class SkyAgGridCellEditorAutocompleteComponent @ViewChild('skyCellEditorAutocomplete', { read: ElementRef }) public input: ElementRef | undefined; - @HostListener('blur') - public onBlur(): void { - this.#stopEditingOnBlur(); + @HostListener('focusout', ['$event']) + public onBlur(event: FocusEvent): void { + if ( + event.relatedTarget && + event.relatedTarget === this.#params?.eGridCell + ) { + // If focus is being set to the grid cell, schedule focus on the input. + // This happens when the refreshCells API is called. + this.afterGuiAttached(); + } else { + this.#stopEditingOnBlur(); + } } public agInit(params: SkyCellEditorAutocompleteParams): void { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.spec.ts index 0c6ca7f41c..6c57dc89cf 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.spec.ts @@ -160,6 +160,7 @@ describe('SkyCellEditorCurrencyComponent', () => { describe('afterGuiAttached', () => { let cellEditorParams: Partial; let column: AgColumn; + let gridCell: HTMLDivElement; const rowNode = new RowNode({} as BeanCollection); rowNode.rowHeight = 37; const value = 15; @@ -174,16 +175,19 @@ describe('SkyCellEditorCurrencyComponent', () => { true, ); - const api = {} as GridApi; + const api = jasmine.createSpyObj([ + 'getDisplayNameForColumn', + 'stopEditing', + ]); - api.getDisplayNameForColumn = (): string => { - return ''; - }; + api.getDisplayNameForColumn.and.returnValue(''); + gridCell = document.createElement('div'); cellEditorParams = { api, value: value, column, + eGridCell: gridCell, node: rowNode, colDef: {}, cellStartedEdit: true, @@ -514,6 +518,24 @@ describe('SkyCellEditorCurrencyComponent', () => { expect(input).toBeVisible(); expect(input.focus).toHaveBeenCalled(); })); + + it('should respond to refocus', fakeAsync(() => { + currencyEditorComponent.agInit(cellEditorParams as ICellEditorParams); + currencyEditorFixture.detectChanges(); + + const input = currencyEditorFixture.nativeElement.querySelector( + 'input', + ) as HTMLInputElement; + spyOn(input, 'focus'); + + currencyEditorComponent.onFocusOut({ + relatedTarget: gridCell, + } as unknown as FocusEvent); + tick(); + expect(input).toBeVisible(); + expect(input.focus).toHaveBeenCalled(); + expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled(); + })); }); it('returns undefined if the value is not set', () => { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.ts index f42caa6b10..8a816439f7 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-currency/cell-editor-currency.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, ElementRef, + HostListener, ViewChild, inject, } from '@angular/core'; @@ -46,6 +47,15 @@ export class SkyAgGridCellEditorCurrencyComponent @ViewChild('skyCellEditorCurrency', { read: ElementRef }) public input: ElementRef | undefined; + @HostListener('focusout', ['$event']) + public onFocusOut(event: FocusEvent): void { + if (event.relatedTarget && event.relatedTarget === this.params?.eGridCell) { + // If focus is being set to the grid cell, schedule focus on the input. + // This happens when the refreshCells API is called. + this.afterGuiAttached(); + } + } + #triggerType: SkyAgGridCellEditorInitialAction | undefined; readonly #changeDetector = inject(ChangeDetectorRef); #initialized = false; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.spec.ts index d611816d70..558b74c032 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.spec.ts @@ -24,6 +24,7 @@ import { SkyAgGridCellEditorLookupComponent } from './cell-editor-lookup.compone import { SkyAgGridCellEditorLookupModule } from './cell-editor-lookup.module'; describe('SkyAgGridCellEditorLookupComponent', () => { + let api: jasmine.SpyObj; let component: SkyAgGridCellEditorLookupComponent; const data = [ { id: '1', name: 'John Doe', town: 'Daniel Island' }, @@ -32,6 +33,7 @@ describe('SkyAgGridCellEditorLookupComponent', () => { { id: '4', name: 'Jane Smith', town: 'Mt Pleasant' }, ]; let fixture: ComponentFixture; + let gridCell: HTMLDivElement; let nativeElement: HTMLElement; let callback: ((args: Record) => void) | undefined; const selection = [data[0]]; @@ -52,18 +54,21 @@ describe('SkyAgGridCellEditorLookupComponent', () => { ], }); + api = jasmine.createSpyObj('GridApi', [ + 'addEventListener', + 'getGridOption', + 'stopEditing', + ]); + api.addEventListener.and.callFake( + (_event: string, listener: (params: any) => void) => { + callback = listener; + }, + ); + api.getGridOption.and.returnValue(true); + gridCell = document.createElement('div'); + cellEditorParams = { - api: { - addEventListener: ( - event: string, - listener: (args: Record) => void, - ) => { - callback = listener; - [event].pop(); - }, - getGridOption: jasmine.createSpy('getGridOption').and.returnValue(true), - stopEditing: jasmine.createSpy('stopEditing'), - } as unknown as GridApi, + api, cellStartedEdit: true, colDef: { headerName: 'header', @@ -76,6 +81,7 @@ describe('SkyAgGridCellEditorLookupComponent', () => { gridOptions: {} as Partial, }, data: undefined, + eGridCell: gridCell, formatValue: jasmine.createSpy('formatValue'), onKeyDown: jasmine.createSpy('onKeyDown'), parseValue: jasmine.createSpy('parseValue'), @@ -304,7 +310,7 @@ describe('SkyAgGridCellEditorLookupComponent', () => { expect(cellEditorParams.api?.stopEditing).toHaveBeenCalledTimes(1); (cellEditorParams.api?.stopEditing as jasmine.Spy).calls.reset(); - component.onBlur(); + component.onBlur({} as FocusEvent); tick(); expect(cellEditorParams.api?.getGridOption).toHaveBeenCalledTimes(2); expect( @@ -317,6 +323,27 @@ describe('SkyAgGridCellEditorLookupComponent', () => { ]); expect(cellEditorParams.api?.stopEditing).toHaveBeenCalledTimes(1); })); + + it('should respond to refocus', fakeAsync(() => { + component.agInit(cellEditorParams as ICellEditorParams); + fixture.detectChanges(); + + const input = nativeElement.querySelector( + 'textarea', + ) as HTMLTextAreaElement; + spyOn(input, 'focus'); + + component.afterGuiAttached(); + tick(); + + component.onBlur({ + relatedTarget: gridCell, + } as unknown as FocusEvent); + tick(); + expect(input).toBeVisible(); + expect(input.focus).toHaveBeenCalled(); + expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled(); + })); }); describe('cellStartedEdit is false', () => { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.ts index a05020fa8d..faabee30d8 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-lookup/cell-editor-lookup.component.ts @@ -52,9 +52,18 @@ export class SkyAgGridCellEditorLookupComponent #changeDetector = inject(ChangeDetectorRef); #elementRef = inject(ElementRef); - @HostListener('blur') - public onBlur(): void { - this.#stopEditingOnBlur(); + @HostListener('focusout', ['$event']) + public onBlur(event: FocusEvent): void { + if ( + event.relatedTarget && + event.relatedTarget === this.#params?.eGridCell + ) { + // If focus is being set to the grid cell, schedule focus on the input. + // This happens when the refreshCells API is called. + this.afterGuiAttached(); + } else { + this.#stopEditingOnBlur(); + } } public agInit(params: SkyCellEditorLookupParams): void { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.spec.ts index 84258c08d4..6134440af4 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.spec.ts @@ -1,4 +1,9 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; import { expect, expectAsync } from '@skyux-sdk/testing'; import { @@ -331,6 +336,7 @@ describe('SkyCellEditorNumberComponent', () => { describe('afterGuiAttached', () => { let cellEditorParams: Partial; let column: AgColumn; + let gridCell: HTMLDivElement; const rowNode = new RowNode({} as BeanCollection); rowNode.rowHeight = 37; const value = 15; @@ -351,10 +357,13 @@ describe('SkyCellEditorNumberComponent', () => { return ''; }; + gridCell = document.createElement('div'); + cellEditorParams = { api: gridApi, value: value, column, + eGridCell: gridCell, node: rowNode, colDef: {}, cellStartedEdit: true, @@ -365,7 +374,7 @@ describe('SkyCellEditorNumberComponent', () => { }; }); - it('focuses on the input after it attaches to the DOM', () => { + it('focuses on the input after it attaches to the DOM', fakeAsync(() => { numberEditorFixture.detectChanges(); const input = numberEditorNativeElement.querySelector( @@ -374,10 +383,31 @@ describe('SkyCellEditorNumberComponent', () => { spyOn(input, 'focus'); numberEditorComponent.afterGuiAttached(); + tick(); expect(input).toBeVisible(); expect(input.focus).toHaveBeenCalled(); - }); + })); + + it('should respond to refocus', fakeAsync(() => { + numberEditorComponent.agInit(cellEditorParams as ICellEditorParams); + numberEditorFixture.detectChanges(); + + const input = numberEditorNativeElement.querySelector( + 'input', + ) as HTMLInputElement; + spyOn(input, 'focus'); + + numberEditorComponent.afterGuiAttached(); + tick(); + + numberEditorComponent.onFocusOut({ + relatedTarget: gridCell, + } as unknown as FocusEvent); + tick(); + expect(input).toBeVisible(); + expect(input.focus).toHaveBeenCalled(); + })); describe('cellStartedEdit is true', () => { it('does not select the input value if Backspace triggers the edit', () => { @@ -431,7 +461,7 @@ describe('SkyCellEditorNumberComponent', () => { expect(selectSpy).not.toHaveBeenCalled(); }); - it('selects the input value if Enter triggers the edit', () => { + it('selects the input value if Enter triggers the edit', fakeAsync(() => { numberEditorComponent.agInit({ ...(cellEditorParams as ICellEditorParams), eventKey: KeyCode.ENTER, @@ -443,10 +473,11 @@ describe('SkyCellEditorNumberComponent', () => { const selectSpy = spyOn(input, 'select'); numberEditorComponent.afterGuiAttached(); + tick(); expect(input.value).toBe('15'); expect(selectSpy).toHaveBeenCalled(); - }); + })); it('does not select the input value when a standard keyboard event triggers the edit', () => { numberEditorComponent.agInit({ diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.ts index 1f2d5e3489..e7fbfabe39 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-number/cell-editor-number.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, + HostListener, ViewChild, } from '@angular/core'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; @@ -39,6 +40,18 @@ export class SkyAgGridCellEditorNumberComponent @ViewChild('skyCellEditorNumber', { read: ElementRef }) public input: ElementRef | undefined; + @HostListener('focusout', ['$event']) + public onFocusOut(event: FocusEvent): void { + if ( + event.relatedTarget && + event.relatedTarget === this.#params?.eGridCell + ) { + // If focus is being set to the grid cell, schedule focus on the input. + // This happens when the refreshCells API is called. + this.afterGuiAttached(); + } + } + #params: ICellEditorParams | undefined; #triggerType: SkyAgGridCellEditorInitialAction | undefined; @@ -84,12 +97,17 @@ export class SkyAgGridCellEditorNumberComponent * afterGuiAttached is called by agGrid after the editor is rendered in the DOM. Once it is attached the editor is ready to be focused on. */ public afterGuiAttached(): void { - if (this.input) { - this.input.nativeElement.focus(); - if (this.#triggerType === SkyAgGridCellEditorInitialAction.Highlighted) { - this.input.nativeElement.select(); + // AG Grid sets focus to the cell via setTimeout, and this queues the input to focus after that. + setTimeout(() => { + if (this.input) { + this.input.nativeElement.focus(); + if ( + this.#triggerType === SkyAgGridCellEditorInitialAction.Highlighted + ) { + this.input.nativeElement.select(); + } } - } + }); } /** diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.spec.ts index 7256b2f62c..14f093b0dd 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.spec.ts @@ -69,6 +69,7 @@ describe('SkyCellEditorTextComponent', () => { ]); let cellEditorParams: Partial; let column: AgColumn; + let gridCell: HTMLDivElement; const rowNode = new RowNode({} as BeanCollection); rowNode.rowHeight = 37; const value = 'testing'; @@ -83,10 +84,13 @@ describe('SkyCellEditorTextComponent', () => { true, ); + gridCell = document.createElement('div'); + cellEditorParams = { api, value: value, column, + eGridCell: gridCell, node: rowNode, colDef: {}, cellStartedEdit: true, @@ -123,6 +127,26 @@ describe('SkyCellEditorTextComponent', () => { ); }); + it('should respond to refocus', fakeAsync(() => { + textEditorComponent.agInit(cellEditorParams as ICellEditorParams); + textEditorFixture.detectChanges(); + + const input = textEditorNativeElement.querySelector( + 'input', + ) as HTMLInputElement; + spyOn(input, 'focus'); + + textEditorComponent.afterGuiAttached(); + tick(); + + textEditorComponent.onFocusOut({ + relatedTarget: gridCell, + } as unknown as FocusEvent); + tick(); + expect(input).toBeVisible(); + expect(input.focus).toHaveBeenCalled(); + })); + describe('cellStartedEdit is true', () => { it('initializes with a cleared value when Backspace triggers the edit', () => { expect(textEditorComponent.editorForm.get('text')?.value).toBeNull(); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.ts index d9312a1b44..b96d74e5f3 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-text/cell-editor-text.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, + HostListener, ViewChild, } from '@angular/core'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; @@ -39,6 +40,18 @@ export class SkyAgGridCellEditorTextComponent @ViewChild('skyCellEditorText', { read: ElementRef }) public input: ElementRef | undefined; + @HostListener('focusout', ['$event']) + public onFocusOut(event: FocusEvent): void { + if ( + event.relatedTarget && + event.relatedTarget === this.#params?.eGridCell + ) { + // If focus is being set to the grid cell, schedule focus on the input. + // This happens when the refreshCells API is called. + this.afterGuiAttached(); + } + } + #params: ICellEditorParams | undefined; #triggerType: SkyAgGridCellEditorInitialAction | undefined;