From b67e87234aa3234c0d946e97507f2dc2482fb5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Fri, 1 Dec 2023 09:21:42 +0100 Subject: [PATCH] WIP internalize @microsoft/fast-components --- LICENSE | 28 + packages/components/package.json | 2 +- packages/components/src/__test__/README.md | 4 + .../src/__test__/component.schema.json | 100 ++ .../components/src/__test__/setup-browser.ts | 6 + .../components/src/__test__/setup-node.ts | 12 + .../accordion-item/accordion-item.styles.ts | 299 ++-- .../components/src/accordion-item/index.ts | 80 +- .../src/accordion/accordion.stories.ts | 44 +- .../src/accordion/accordion.styles.ts | 30 + packages/components/src/accordion/index.ts | 22 +- .../components/src/anchor/anchor.pw.spec.ts | 53 + .../components/src/anchor/anchor.stories.ts | 47 +- .../components/src/anchor/anchor.styles.ts | 29 + packages/components/src/anchor/index.ts | 93 +- .../anchored-region/anchored-region.styles.ts | 16 + .../components/src/anchored-region/index.ts | 21 +- .../components/src/avatar/avatar.stories.ts | 79 +- .../components/src/avatar/avatar.styles.ts | 192 +-- packages/components/src/avatar/index.ts | 84 +- .../components/src/badge/badge.stories.ts | 63 +- packages/components/src/badge/badge.styles.ts | 91 +- packages/components/src/badge/index.ts | 17 +- .../breadcrumb-item.stories.ts | 59 +- .../breadcrumb-item/breadcrumb-item.styles.ts | 75 +- .../components/src/breadcrumb-item/index.ts | 33 +- .../src/breadcrumb/breadcrumb.stories.ts | 96 +- packages/components/src/breadcrumb/index.ts | 22 +- packages/components/src/button/README.md | 99 -- .../src/button/button.open-ui.definition.ts | 4 + .../components/src/button/button.pw.spec.ts | 52 + .../components/src/button/button.stories.ts | 126 +- .../components/src/button/button.styles.ts | 821 ++-------- packages/components/src/button/index.ts | 122 +- .../src/calendar/calendar.stories.ts | 8 + .../src/calendar/calendar.styles.ts | 151 ++ packages/components/src/calendar/index.ts | 30 + packages/components/src/card/card.stories.ts | 41 +- packages/components/src/card/card.styles.ts | 41 + packages/components/src/card/index.ts | 49 +- .../src/checkbox/checkbox.stories.ts | 92 +- .../src/checkbox/checkbox.styles.ts | 467 +++--- packages/components/src/checkbox/index.ts | 53 +- packages/components/src/color/README.md | 33 + packages/components/src/color/palette.spec.ts | 29 + packages/components/src/color/palette.ts | 200 +++ packages/components/src/color/recipe.ts | 24 + .../src/color/recipes/accent-fill.ts | 40 + .../color/recipes/accent-foreground.spec.ts | 95 ++ .../src/color/recipes/accent-foreground.ts | 57 + .../src/color/recipes/focus-stroke.ts | 22 + .../recipes/foreground-on-accent.spec.ts | 19 + .../src/color/recipes/foreground-on-accent.ts | 9 + .../color/recipes/neutral-fill-contrast.ts | 42 + .../src/color/recipes/neutral-fill-input.ts | 26 + .../color/recipes/neutral-fill-layer.spec.ts | 31 + .../src/color/recipes/neutral-fill-layer.ts | 15 + .../src/color/recipes/neutral-fill-stealth.ts | 40 + .../src/color/recipes/neutral-fill.ts | 33 + .../recipes/neutral-foreground-hint.spec.ts | 35 + .../color/recipes/neutral-foreground-hint.ts | 13 + .../color/recipes/neutral-foreground.spec.ts | 22 + .../src/color/recipes/neutral-foreground.ts | 9 + .../src/color/recipes/neutral-layer-1.ts | 9 + .../src/color/recipes/neutral-layer-2.ts | 45 + .../src/color/recipes/neutral-layer-3.ts | 26 + .../src/color/recipes/neutral-layer-4.ts | 27 + .../recipes/neutral-layer-card-container.ts | 16 + .../color/recipes/neutral-layer-floating.ts | 16 + .../src/color/recipes/neutral-layer.spec.ts | 72 + .../color/recipes/neutral-stroke-divider.ts | 21 + .../src/color/recipes/neutral-stroke.ts | 31 + packages/components/src/color/swatch.spec.ts | 38 + packages/components/src/color/swatch.ts | 77 + .../color/utilities/base-layer-luminance.ts | 22 + .../src/color/utilities/binary-search.ts | 31 + .../src/color/utilities/color-constants.ts | 23 + .../color/utilities/direction-by-is-dark.ts | 9 + .../components/src/color/utilities/is-dark.ts | 20 + .../src/color/utilities/relative-luminance.ts | 19 + .../src/combobox/combobox.stories.ts | 121 +- .../src/combobox/combobox.styles.ts | 94 +- packages/components/src/combobox/index.ts | 165 +- packages/components/src/custom-elements.ts | 171 +- .../src/data-grid/data-grid-cell.styles.ts | 112 +- .../src/data-grid/data-grid-row.styles.ts | 33 + .../src/data-grid/data-grid.stories.ts | 545 ++++++- .../src/data-grid/data-grid.styles.ts | 17 + packages/components/src/data-grid/index.ts | 52 +- .../src/design-system-provider/README.md | 68 + .../src/design-system-provider/index.ts | 1087 +++++++++++++ packages/components/src/design-tokens.ts | 1430 +++++++++++++---- .../components/src/dialog/dialog.pw.spec.ts | 98 ++ .../components/src/dialog/dialog.stories.ts | 67 +- .../components/src/dialog/dialog.styles.ts | 57 + packages/components/src/dialog/index.ts | 18 +- .../src/disclosure/disclosure.stories.ts | 8 + .../src/disclosure/disclosure.styles.ts | 91 ++ .../src/disclosure/fixtures/disclosure.html | 84 + packages/components/src/disclosure/index.ts | 101 ++ .../src/disclosure/scenarios/index.html | 17 + .../components/src/divider/divider.stories.ts | 50 +- .../components/src/divider/divider.styles.ts | 28 + packages/components/src/divider/index.ts | 20 +- .../src/flipper/fixtures/flipper.html | 45 + .../components/src/flipper/flipper.stories.ts | 8 + .../components/src/flipper/flipper.styles.ts | 168 ++ packages/components/src/flipper/index.ts | 43 + .../src/flipper/scenarios/index.html | 39 + .../src/horizontal-scroll/README.md | 4 + .../fixtures/horizontal-scroll.html | 415 +++++ .../horizontal-scroll.pw.spec.ts | 322 ++++ .../horizontal-scroll.stories.ts | 8 + .../horizontal-scroll.styles.ts | 140 ++ .../components/src/horizontal-scroll/index.ts | 58 + packages/components/src/index-rollup.ts | 16 +- packages/components/src/index.ts | 102 +- packages/components/src/listbox/index.ts | 81 +- .../components/src/listbox/listbox.pw.spec.ts | 103 ++ .../components/src/listbox/listbox.stories.ts | 48 +- .../components/src/listbox/listbox.styles.ts | 73 +- packages/components/src/menu-item/index.ts | 77 +- .../src/menu-item/menu-item.stories.ts | 69 +- .../src/menu-item/menu-item.styles.ts | 775 +++++---- packages/components/src/menu/index.ts | 39 +- packages/components/src/menu/menu.stories.ts | 90 +- packages/components/src/menu/menu.styles.ts | 59 + packages/components/src/number-field/index.ts | 67 +- .../src/number-field/number-field.stories.ts | 149 +- .../src/number-field/number-field.styles.ts | 268 ++- packages/components/src/option/index.ts | 41 +- .../components/src/option/option.stories.ts | 70 +- .../components/src/option/option.styles.ts | 310 ++-- packages/components/src/picker/README.md | 1 + .../src/picker/fixtures/picker.html | 162 ++ packages/components/src/picker/index.ts | 114 ++ .../picker-list-item.open-ui.definition.ts | 4 + .../src/picker/picker-list-item.styles.ts | 99 ++ .../picker-list-item.vscode.definition.ts | 25 + .../picker/picker-list.open-ui.definition.ts | 4 + .../src/picker/picker-list.styles.ts | 82 + .../picker/picker-list.vscode.definition.ts | 32 + .../picker-menu-option.open-ui.definition.ts | 4 + .../src/picker/picker-menu-option.styles.ts | 114 ++ .../picker-menu-option.vscode.definition.ts | 25 + .../picker/picker-menu.open-ui.definition.ts | 4 + .../src/picker/picker-menu.styles.ts | 59 + .../picker/picker-menu.vscode.definition.ts | 25 + .../src/picker/picker.open-ui.definition.ts | 4 + .../components/src/picker/picker.stories.ts | 33 + .../components/src/picker/picker.styles.ts | 57 + .../src/picker/picker.vscode.definition.ts | 114 ++ .../src/picker/scenarios/index.html | 11 + .../components/src/progress-ring/index.ts | 27 +- .../progress-ring/progress-ring.stories.ts | 62 +- .../src/progress-ring/progress-ring.styles.ts | 106 ++ packages/components/src/progress/index.ts | 31 +- .../src/progress/progress.stories.ts | 67 +- .../src/progress/progress.styles.ts | 147 ++ packages/components/src/radio-group/index.ts | 20 +- .../src/radio-group/radio-group.stories.ts | 82 +- .../src/radio-group/radio-group.styles.ts | 28 + packages/components/src/radio/index.ts | 29 +- .../components/src/radio/radio.stories.ts | 75 +- packages/components/src/radio/radio.styles.ts | 429 +++-- packages/components/src/search/index.ts | 57 +- .../components/src/search/search.stories.ts | 139 +- .../components/src/search/search.styles.ts | 414 +++-- packages/components/src/select/index.ts | 259 +-- .../components/src/select/select.pw.spec.ts | 322 ++++ .../components/src/select/select.stories.ts | 103 +- .../components/src/select/select.styles.ts | 562 +++---- .../src/skeleton/fixtures/base.html | 120 ++ packages/components/src/skeleton/index.ts | 25 + .../src/skeleton/scenarios/index.html | 53 + .../src/skeleton/skeleton.stories.ts | 8 + .../src/skeleton/skeleton.styles.ts | 106 ++ packages/components/src/slider-label/index.ts | 54 +- .../src/slider-label/slider-label.stories.ts | 52 +- .../src/slider-label/slider-label.styles.ts | 122 ++ .../src/slider/images/slider-rtl.png | Bin 0 -> 5274 bytes packages/components/src/slider/index.ts | 27 +- .../components/src/slider/slider.stories.ts | 91 +- .../components/src/slider/slider.styles.ts | 342 ++-- packages/components/src/styles/direction.ts | 102 ++ packages/components/src/styles/elevation.ts | 8 +- packages/components/src/styles/index.ts | 10 +- .../components/src/styles/patterns/button.ts | 501 ++++++ .../components/src/styles/patterns/index.ts | 5 +- packages/components/src/styles/size.ts | 8 +- packages/components/src/switch/index.ts | 29 +- .../components/src/switch/switch.stories.ts | 91 +- .../components/src/switch/switch.styles.ts | 478 +++--- packages/components/src/tab-panel/index.ts | 20 +- .../src/tab-panel/tab-panel.styles.ts | 24 + packages/components/src/tab/index.ts | 17 +- packages/components/src/tab/tab.styles.ts | 226 ++- packages/components/src/tabs/index.ts | 21 +- packages/components/src/tabs/tabs.stories.ts | 96 +- packages/components/src/tabs/tabs.styles.ts | 232 +-- packages/components/src/text-area/index.ts | 54 +- .../src/text-area/text-area.stories.ts | 111 +- .../src/text-area/text-area.styles.ts | 182 ++- packages/components/src/text-field/README.md | 4 + .../src/text-field/fixtures/text-field.html | 118 ++ packages/components/src/text-field/index.ts | 62 +- .../src/text-field/scenarios/index.html | 3 + .../text-field.open-ui.definition.ts | 4 + .../src/text-field/text-field.stories.ts | 162 +- .../src/text-field/text-field.styles.ts | 206 ++- .../text-field.vscode.definition.ts | 160 ++ packages/components/src/toolbar/index.ts | 56 +- .../components/src/toolbar/toolbar.stories.ts | 57 +- .../components/src/toolbar/toolbar.styles.ts | 143 +- packages/components/src/tooltip/index.ts | 20 +- .../components/src/tooltip/tooltip.stories.ts | 122 +- .../components/src/tooltip/tooltip.styles.ts | 116 ++ packages/components/src/tree-item/index.ts | 27 +- .../src/tree-item/tree-item.stories.ts | 59 +- .../src/tree-item/tree-item.styles.ts | 631 ++++---- packages/components/src/tree-view/index.ts | 20 +- .../src/tree-view/tree-view.stories.ts | 77 +- .../src/tree-view/tree-view.styles.ts | 22 + .../components/src/utilities/behaviors.ts | 15 + 224 files changed, 14893 insertions(+), 7708 deletions(-) create mode 100644 packages/components/src/__test__/README.md create mode 100644 packages/components/src/__test__/component.schema.json create mode 100644 packages/components/src/__test__/setup-browser.ts create mode 100644 packages/components/src/__test__/setup-node.ts create mode 100644 packages/components/src/accordion/accordion.styles.ts create mode 100644 packages/components/src/anchor/anchor.pw.spec.ts create mode 100644 packages/components/src/anchor/anchor.styles.ts create mode 100644 packages/components/src/anchored-region/anchored-region.styles.ts delete mode 100644 packages/components/src/button/README.md create mode 100644 packages/components/src/button/button.open-ui.definition.ts create mode 100644 packages/components/src/button/button.pw.spec.ts create mode 100644 packages/components/src/calendar/calendar.stories.ts create mode 100644 packages/components/src/calendar/calendar.styles.ts create mode 100644 packages/components/src/calendar/index.ts create mode 100644 packages/components/src/card/card.styles.ts create mode 100644 packages/components/src/color/README.md create mode 100644 packages/components/src/color/palette.spec.ts create mode 100644 packages/components/src/color/palette.ts create mode 100644 packages/components/src/color/recipe.ts create mode 100644 packages/components/src/color/recipes/accent-fill.ts create mode 100644 packages/components/src/color/recipes/accent-foreground.spec.ts create mode 100644 packages/components/src/color/recipes/accent-foreground.ts create mode 100644 packages/components/src/color/recipes/focus-stroke.ts create mode 100644 packages/components/src/color/recipes/foreground-on-accent.spec.ts create mode 100644 packages/components/src/color/recipes/foreground-on-accent.ts create mode 100644 packages/components/src/color/recipes/neutral-fill-contrast.ts create mode 100644 packages/components/src/color/recipes/neutral-fill-input.ts create mode 100644 packages/components/src/color/recipes/neutral-fill-layer.spec.ts create mode 100644 packages/components/src/color/recipes/neutral-fill-layer.ts create mode 100644 packages/components/src/color/recipes/neutral-fill-stealth.ts create mode 100644 packages/components/src/color/recipes/neutral-fill.ts create mode 100644 packages/components/src/color/recipes/neutral-foreground-hint.spec.ts create mode 100644 packages/components/src/color/recipes/neutral-foreground-hint.ts create mode 100644 packages/components/src/color/recipes/neutral-foreground.spec.ts create mode 100644 packages/components/src/color/recipes/neutral-foreground.ts create mode 100644 packages/components/src/color/recipes/neutral-layer-1.ts create mode 100644 packages/components/src/color/recipes/neutral-layer-2.ts create mode 100644 packages/components/src/color/recipes/neutral-layer-3.ts create mode 100644 packages/components/src/color/recipes/neutral-layer-4.ts create mode 100644 packages/components/src/color/recipes/neutral-layer-card-container.ts create mode 100644 packages/components/src/color/recipes/neutral-layer-floating.ts create mode 100644 packages/components/src/color/recipes/neutral-layer.spec.ts create mode 100644 packages/components/src/color/recipes/neutral-stroke-divider.ts create mode 100644 packages/components/src/color/recipes/neutral-stroke.ts create mode 100644 packages/components/src/color/swatch.spec.ts create mode 100644 packages/components/src/color/swatch.ts create mode 100644 packages/components/src/color/utilities/base-layer-luminance.ts create mode 100644 packages/components/src/color/utilities/binary-search.ts create mode 100644 packages/components/src/color/utilities/color-constants.ts create mode 100644 packages/components/src/color/utilities/direction-by-is-dark.ts create mode 100644 packages/components/src/color/utilities/is-dark.ts create mode 100644 packages/components/src/color/utilities/relative-luminance.ts create mode 100644 packages/components/src/data-grid/data-grid-row.styles.ts create mode 100644 packages/components/src/data-grid/data-grid.styles.ts create mode 100644 packages/components/src/design-system-provider/README.md create mode 100644 packages/components/src/design-system-provider/index.ts create mode 100644 packages/components/src/dialog/dialog.pw.spec.ts create mode 100644 packages/components/src/dialog/dialog.styles.ts create mode 100644 packages/components/src/disclosure/disclosure.stories.ts create mode 100644 packages/components/src/disclosure/disclosure.styles.ts create mode 100644 packages/components/src/disclosure/fixtures/disclosure.html create mode 100644 packages/components/src/disclosure/index.ts create mode 100644 packages/components/src/disclosure/scenarios/index.html create mode 100644 packages/components/src/divider/divider.styles.ts create mode 100644 packages/components/src/flipper/fixtures/flipper.html create mode 100644 packages/components/src/flipper/flipper.stories.ts create mode 100644 packages/components/src/flipper/flipper.styles.ts create mode 100644 packages/components/src/flipper/index.ts create mode 100644 packages/components/src/flipper/scenarios/index.html create mode 100644 packages/components/src/horizontal-scroll/README.md create mode 100644 packages/components/src/horizontal-scroll/fixtures/horizontal-scroll.html create mode 100644 packages/components/src/horizontal-scroll/horizontal-scroll.pw.spec.ts create mode 100644 packages/components/src/horizontal-scroll/horizontal-scroll.stories.ts create mode 100644 packages/components/src/horizontal-scroll/horizontal-scroll.styles.ts create mode 100644 packages/components/src/horizontal-scroll/index.ts create mode 100644 packages/components/src/listbox/listbox.pw.spec.ts create mode 100644 packages/components/src/menu/menu.styles.ts create mode 100644 packages/components/src/picker/README.md create mode 100644 packages/components/src/picker/fixtures/picker.html create mode 100644 packages/components/src/picker/index.ts create mode 100644 packages/components/src/picker/picker-list-item.open-ui.definition.ts create mode 100644 packages/components/src/picker/picker-list-item.styles.ts create mode 100644 packages/components/src/picker/picker-list-item.vscode.definition.ts create mode 100644 packages/components/src/picker/picker-list.open-ui.definition.ts create mode 100644 packages/components/src/picker/picker-list.styles.ts create mode 100644 packages/components/src/picker/picker-list.vscode.definition.ts create mode 100644 packages/components/src/picker/picker-menu-option.open-ui.definition.ts create mode 100644 packages/components/src/picker/picker-menu-option.styles.ts create mode 100644 packages/components/src/picker/picker-menu-option.vscode.definition.ts create mode 100644 packages/components/src/picker/picker-menu.open-ui.definition.ts create mode 100644 packages/components/src/picker/picker-menu.styles.ts create mode 100644 packages/components/src/picker/picker-menu.vscode.definition.ts create mode 100644 packages/components/src/picker/picker.open-ui.definition.ts create mode 100644 packages/components/src/picker/picker.stories.ts create mode 100644 packages/components/src/picker/picker.styles.ts create mode 100644 packages/components/src/picker/picker.vscode.definition.ts create mode 100644 packages/components/src/picker/scenarios/index.html create mode 100644 packages/components/src/progress-ring/progress-ring.styles.ts create mode 100644 packages/components/src/progress/progress.styles.ts create mode 100644 packages/components/src/radio-group/radio-group.styles.ts create mode 100644 packages/components/src/select/select.pw.spec.ts create mode 100644 packages/components/src/skeleton/fixtures/base.html create mode 100644 packages/components/src/skeleton/index.ts create mode 100644 packages/components/src/skeleton/scenarios/index.html create mode 100644 packages/components/src/skeleton/skeleton.stories.ts create mode 100644 packages/components/src/skeleton/skeleton.styles.ts create mode 100644 packages/components/src/slider-label/slider-label.styles.ts create mode 100644 packages/components/src/slider/images/slider-rtl.png create mode 100644 packages/components/src/styles/direction.ts create mode 100644 packages/components/src/styles/patterns/button.ts create mode 100644 packages/components/src/tab-panel/tab-panel.styles.ts create mode 100644 packages/components/src/text-field/README.md create mode 100644 packages/components/src/text-field/fixtures/text-field.html create mode 100644 packages/components/src/text-field/scenarios/index.html create mode 100644 packages/components/src/text-field/text-field.open-ui.definition.ts create mode 100644 packages/components/src/text-field/text-field.vscode.definition.ts create mode 100644 packages/components/src/tooltip/tooltip.styles.ts create mode 100644 packages/components/src/tree-view/tree-view.styles.ts create mode 100644 packages/components/src/utilities/behaviors.ts diff --git a/LICENSE b/LICENSE index ed61467d..4643ba48 100644 --- a/LICENSE +++ b/LICENSE @@ -25,3 +25,31 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- + +This project code source is modified from `@microsoft/fast-components` licensed under + +FAST - https://www.fast.design/ + +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/packages/components/package.json b/packages/components/package.json index 44d240c1..173e7b5a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -21,6 +21,7 @@ "main": "dist/esm/index.js", "types": "dist/dts/index.d.ts", "sideEffects": false, + "type": "module", "scripts": { "start": "storybook dev -p 6006", "start:ci": "storybook dev -p 6006 --ci --quiet", @@ -41,7 +42,6 @@ }, "dependencies": { "@microsoft/fast-colors": "^5.3.1", - "@microsoft/fast-components": "^2.30.6", "@microsoft/fast-element": "^1.12.0", "@microsoft/fast-foundation": "^2.49.4", "@microsoft/fast-web-utilities": "^5.4.1" diff --git a/packages/components/src/__test__/README.md b/packages/components/src/__test__/README.md new file mode 100644 index 00000000..7f7e750b --- /dev/null +++ b/packages/components/src/__test__/README.md @@ -0,0 +1,4 @@ +## Component schema + +The `component.schema.json` is used to test against JSON schemas for each component. It should mirror the definition created by [Open UI](https://github.com/WICG/open-ui/blob/master/research/src/schemas/component.schema.json5). +s \ No newline at end of file diff --git a/packages/components/src/__test__/component.schema.json b/packages/components/src/__test__/component.schema.json new file mode 100644 index 00000000..c2fdbaed --- /dev/null +++ b/packages/components/src/__test__/component.schema.json @@ -0,0 +1,100 @@ +{ + "$id": "component.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Component", + "description": "A UI Component definition.", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "The name of the component as defined in the design system.", + "type": "string" + }, + "openUIName": { + "description": "The name of the component as defined in OpenUI.", + "type": "string" + }, + "url": { + "description": "The url to the component doc page.", + "type": "string" + }, + "definition": { + "type": "string", + "description": "This should be the formal definition of the component. It usually describes it's intended purpose." + }, + "anatomy": { + "type": "array", + "items": { + "type": "object", + "description": "Each named part that makes up the whole of the component.", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "uniqueItems": true + }, + "implementations": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "web-component" + }, + "implementation": { + "$ref": "vscode-html-customdata" + } + }, + "required": [ + "type", + "implementation" + ] + } + ] + } + }, + "concepts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "category": { + "type": "string" + }, + "openUIName": { + "type": "string" + }, + "image": { + "type": "string" + } + }, + "required": [ + "name", + "image" + ] + } + } + }, + "required": [ + "name", + "url" + ] +} \ No newline at end of file diff --git a/packages/components/src/__test__/setup-browser.ts b/packages/components/src/__test__/setup-browser.ts new file mode 100644 index 00000000..55b02eb6 --- /dev/null +++ b/packages/components/src/__test__/setup-browser.ts @@ -0,0 +1,6 @@ +function importAll(r: __WebpackModuleApi.RequireContext): void { + r.keys().forEach(r); +} + +// Explicitly add to browser test +importAll(require.context("../", true, /\.spec\.js$/)); diff --git a/packages/components/src/__test__/setup-node.ts b/packages/components/src/__test__/setup-node.ts new file mode 100644 index 00000000..be4102c4 --- /dev/null +++ b/packages/components/src/__test__/setup-node.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +if (window.document && !window.document.createRange) { + window.document.createRange = () => ({ + setStart: () => {}, + setEnd: () => {}, + // @ts-ignore + commonAncestorContainer: { + nodeName: "BODY", + ownerDocument: document, + }, + }); +} diff --git a/packages/components/src/accordion-item/accordion-item.styles.ts b/packages/components/src/accordion-item/accordion-item.styles.ts index 3ac505ad..c5805191 100644 --- a/packages/components/src/accordion-item/accordion-item.styles.ts +++ b/packages/components/src/accordion-item/accordion-item.styles.ts @@ -1,161 +1,156 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - AccordionItemOptions, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + AccordionItemOptions, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillFocus, - bodyFont, - controlCornerRadius, - density, - designUnit, - focusStrokeWidth, - neutralForegroundRest, - neutralStrokeDividerRest, - strokeWidth, - typeRampMinus1FontSize, - typeRampMinus1LineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/size'; + accentForegroundRest, + bodyFont, + controlCornerRadius, + density, + designUnit, + focusStrokeOuter, + focusStrokeWidth, + neutralForegroundRest, + neutralStrokeDividerRest, + strokeWidth, + typeRampMinus1FontSize, + typeRampMinus1LineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/size.js"; /** * Styles for AccordionItem * @public */ export const accordionItemStyles: FoundationElementTemplate< - ElementStyles, - AccordionItemOptions + ElementStyles, + AccordionItemOptions > = (context, definition) => - css` - ${display('flex')} :host { - box-sizing: border-box; - font-family: ${bodyFont}; - flex-direction: column; - font-size: ${typeRampMinus1FontSize}; - line-height: ${typeRampMinus1LineHeight}; - border-bottom: calc(${strokeWidth} * 1px) solid - ${neutralStrokeDividerRest}; - } - - .region { - display: none; - padding: calc((6 + (${designUnit} * 2 * ${density})) * 1px); - } - - div.heading { - display: grid; - position: relative; - grid-template-columns: calc(${heightNumber} * 1px) auto 1fr auto; - color: ${neutralForegroundRest}; - } - - .button { - appearance: none; - border: none; - background: none; - grid-column: 3; - outline: none; - padding: 0 calc((6 + (${designUnit} * 2 * ${density})) * 1px); - text-align: left; - height: calc(${heightNumber} * 1px); - color: currentcolor; - cursor: pointer; - font-family: inherit; - } - - .button:hover { - color: currentcolor; - } - - .button:active { - color: currentcolor; - } - - .button::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - cursor: pointer; - } - - /* prettier-ignore */ - .button:${focusVisible}::before { - outline: none; - border: calc(${focusStrokeWidth} * 1px) solid ${accentFillFocus}; - border-radius: calc(${controlCornerRadius} * 1px); - } - - :host([expanded]) .region { - display: block; - } - - .icon { - display: flex; - align-items: center; - justify-content: center; - grid-column: 1; - grid-row: 1; - pointer-events: none; - position: relative; - } - - slot[name='expanded-icon'], - slot[name='collapsed-icon'] { - fill: currentcolor; - } - - slot[name='collapsed-icon'] { - display: flex; - } - - :host([expanded]) slot[name='collapsed-icon'] { - display: none; - } - - slot[name='expanded-icon'] { - display: none; - } - - :host([expanded]) slot[name='expanded-icon'] { - display: flex; - } - - .start { - display: flex; - align-items: center; - padding-inline-start: calc(${designUnit} * 1px); - justify-content: center; - grid-column: 2; - position: relative; - } - - .end { - display: flex; - align-items: center; - justify-content: center; - grid-column: 4; - position: relative; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - /* prettier-ignore */ - .button:${focusVisible}::before { - border-color: ${SystemColors.Highlight}; - } - :host slot[name='collapsed-icon'], - :host([expanded]) slot[name='expanded-icon'] { - fill: ${SystemColors.ButtonText}; - } - `) - ); + css` + ${display("flex")} :host { + box-sizing: border-box; + font-family: ${bodyFont}; + flex-direction: column; + font-size: ${typeRampMinus1FontSize}; + line-height: ${typeRampMinus1LineHeight}; + border-bottom: calc(${strokeWidth} * 1px) solid ${neutralStrokeDividerRest}; + } + + .region { + display: none; + padding: calc((6 + (${designUnit} * 2 * ${density})) * 1px); + } + + .heading { + display: grid; + position: relative; + grid-template-columns: auto 1fr auto calc(${heightNumber} * 1px); + } + + .button { + appearance: none; + border: none; + background: none; + grid-column: 2; + grid-row: 1; + outline: none; + padding: 0 calc((6 + (${designUnit} * 2 * ${density})) * 1px); + text-align: left; + height: calc(${heightNumber} * 1px); + color: ${neutralForegroundRest}; + cursor: pointer; + font-family: inherit; + } + + .button:hover { + color: ${neutralForegroundRest}; + } + + .button:active { + color: ${neutralForegroundRest}; + } + + .button::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + cursor: pointer; + } + + .button:${focusVisible}::before { + outline: none; + border: calc(${focusStrokeWidth} * 1px) solid ${focusStrokeOuter}; + border-radius: calc(${controlCornerRadius} * 1px); + } + + :host([expanded]) .region { + display: block; + } + + .icon { + display: flex; + align-items: center; + justify-content: center; + grid-column: 4; + pointer-events: none; + position: relative; + } + + slot[name="expanded-icon"], + slot[name="collapsed-icon"] { + fill: ${accentForegroundRest}; + } + + slot[name="collapsed-icon"] { + display: flex; + } + + :host([expanded]) slot[name="collapsed-icon"] { + display: none; + } + + slot[name="expanded-icon"] { + display: none; + } + + :host([expanded]) slot[name="expanded-icon"] { + display: flex; + } + + .start { + display: flex; + align-items: center; + padding-inline-start: calc(${designUnit} * 1px); + justify-content: center; + grid-column: 1; + position: relative; + } + + .end { + display: flex; + align-items: center; + justify-content: center; + grid-column: 3; + position: relative; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .button:${focusVisible}::before { + border-color: ${SystemColors.Highlight}; + } + :host slot[name="collapsed-icon"], + :host([expanded]) slot[name="expanded-icon"] { + fill: ${SystemColors.ButtonText}; + } + ` + ) + ); diff --git a/packages/components/src/accordion-item/index.ts b/packages/components/src/accordion-item/index.ts index c79daf53..5c8ddfba 100644 --- a/packages/components/src/accordion-item/index.ts +++ b/packages/components/src/accordion-item/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - AccordionItem, - AccordionItemOptions, - accordionItemTemplate as template -} from '@microsoft/fast-foundation'; -import { accordionItemStyles as styles } from './accordion-item.styles'; + AccordionItem, + AccordionItemOptions, + accordionItemTemplate as template, +} from "@microsoft/fast-foundation"; +import { accordionItemStyles as styles } from "./accordion-item.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#AccordionItem} registration for configuring the component with a DesignSystem. @@ -15,43 +12,40 @@ import { accordionItemStyles as styles } from './accordion-item.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpAccordionItem = AccordionItem.compose({ - baseName: 'accordion-item', - template, - styles, - collapsedIcon: /* html */ ` - - - +export const fastAccordionItem = AccordionItem.compose({ + baseName: "accordion-item", + template, + styles, + collapsedIcon: /* html */ ` + + + + `, + expandedIcon: /* html */ ` + + + `, - expandedIcon: /* html */ ` - - - - ` }); /** diff --git a/packages/components/src/accordion/accordion.stories.ts b/packages/components/src/accordion/accordion.stories.ts index d70638f5..49bfe9ad 100644 --- a/packages/components/src/accordion/accordion.stories.ts +++ b/packages/components/src/accordion/accordion.stories.ts @@ -1,44 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { HtmlRenderer, Meta, StoryObj, StoryFn } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import Examples from "./fixtures/base.html"; +import "./index.js"; export default { - title: 'Components/Accordion', - - parameters: { - controls: { - disabled: true - }, - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - - - Panel one - Panel one content - - - Panel two - Panel two content - - - Panel three - Panel three content - - `; + title: "Accordion", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = {}; +export const Accordion = () => Examples; diff --git a/packages/components/src/accordion/accordion.styles.ts b/packages/components/src/accordion/accordion.styles.ts new file mode 100644 index 00000000..fb9e3acf --- /dev/null +++ b/packages/components/src/accordion/accordion.styles.ts @@ -0,0 +1,30 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { display, FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { + bodyFont, + neutralForegroundRest, + neutralStrokeDividerRest, + strokeWidth, + typeRampMinus1FontSize, + typeRampMinus1LineHeight, +} from "../design-tokens.js"; + +/** + * Styles for Accordion + * @public + */ +export const accordionStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("flex")} :host { + box-sizing: border-box; + flex-direction: column; + font-family: ${bodyFont}; + font-size: ${typeRampMinus1FontSize}; + line-height: ${typeRampMinus1LineHeight}; + color: ${neutralForegroundRest}; + border-top: calc(${strokeWidth} * 1px) solid ${neutralStrokeDividerRest}; + } + `; diff --git a/packages/components/src/accordion/index.ts b/packages/components/src/accordion/index.ts index f4f07958..0e9799ba 100644 --- a/packages/components/src/accordion/index.ts +++ b/packages/components/src/accordion/index.ts @@ -1,13 +1,7 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import { Accordion, accordionTemplate as template } from "@microsoft/fast-foundation"; +import { accordionStyles as styles } from "./accordion.styles.js"; -import { - Accordion, - accordionTemplate as template -} from '@microsoft/fast-foundation'; -import { accordionStyles as styles } from '@microsoft/fast-components'; - -export * from '../accordion-item/index'; +export * from "../accordion-item/index.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Accordion} registration for configuring the component with a DesignSystem. @@ -16,12 +10,12 @@ export * from '../accordion-item/index'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpAccordion = Accordion.compose({ - baseName: 'accordion', - template, - styles +export const fastAccordion = Accordion.compose({ + baseName: "accordion", + template, + styles, }); /** diff --git a/packages/components/src/anchor/anchor.pw.spec.ts b/packages/components/src/anchor/anchor.pw.spec.ts new file mode 100644 index 00000000..1392e0eb --- /dev/null +++ b/packages/components/src/anchor/anchor.pw.spec.ts @@ -0,0 +1,53 @@ +import type { + Anchor as FASTAnchorType +} from "@microsoft/fast-foundation"; +import chai from "chai"; + +const { expect } = chai; + +type FASTAnchor = HTMLElement & FASTAnchorType; + +describe("FASTAnchor", function () { + beforeEach(async function () { + if (!this.page && !this.browser) { + this.skip(); + } + + this.documentHandle = await this.page.evaluateHandle(() => document); + + this.setupHandle = await this.page.evaluateHandle( + (document) => { + const element = document.createElement("fast-anchor") as FASTAnchor; + element.href = "#"; + element.textContent = "Hello"; + element.id = "anchor1"; + + document.body.appendChild(element) + }, + this.documentHandle + ); + }); + + afterEach(async function () { + if (this.setupHandle) { + await this.setupHandle.dispose(); + } + }); + + // FASTAnchor should render on the page + it("should render on the page", async function () { + const element = await this.page.waitForSelector("fast-anchor"); + + expect(element).to.exist; + }); + + it("receive focus when focused programatically", async function () { + const element = await this.page.waitForSelector("fast-anchor"); + + await this.page.evaluateHandle(element => element.focus(), element) + + expect(await this.page.evaluate( + () => document.activeElement?.id + )).to.equal(await element.evaluate(node => node.id)); + }); +}); diff --git a/packages/components/src/anchor/anchor.stories.ts b/packages/components/src/anchor/anchor.stories.ts index ae88fe5f..7ff1df05 100644 --- a/packages/components/src/anchor/anchor.stories.ts +++ b/packages/components/src/anchor/anchor.stories.ts @@ -1,47 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import AnchorTemplate from "./fixtures/anchor.html"; +import "./index.js"; export default { - title: 'Components/Anchor', - argTypes: { - label: { control: 'text' }, - appearance: { - control: 'select', - options: [ - 'Accent', - 'Lightweight', - 'Neutral', - 'Outline', - 'Stealth', - 'Hypertext' - ] - }, - startIcon: { control: 'boolean' }, - endIcon: { control: 'boolean' } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - ${args.startIcon ? getFaIcon('plus', 'start') : ''}${args.label ?? 'Link'} - ${args.endIcon ? getFaIcon('plus', 'end') : ''} - `; + title: "Anchor", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Link', - appearance: 'Neutral', - startIcon: false, - endIcon: false -}; +export const Anchor = () => AnchorTemplate; diff --git a/packages/components/src/anchor/anchor.styles.ts b/packages/components/src/anchor/anchor.styles.ts new file mode 100644 index 00000000..017c1c8a --- /dev/null +++ b/packages/components/src/anchor/anchor.styles.ts @@ -0,0 +1,29 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { AnchorOptions, FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { + AccentButtonStyles, + BaseButtonStyles, + HypertextStyles, + LightweightButtonStyles, + OutlineButtonStyles, + StealthButtonStyles, +} from "../styles/index.js"; +import { appearanceBehavior } from "../utilities/behaviors.js"; + +/** + * Styles for Anchor + * @public + */ +export const anchorStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${BaseButtonStyles} + `.withBehaviors( + appearanceBehavior("accent", AccentButtonStyles), + appearanceBehavior("hypertext", HypertextStyles), + appearanceBehavior("lightweight", LightweightButtonStyles), + appearanceBehavior("outline", OutlineButtonStyles), + appearanceBehavior("stealth", StealthButtonStyles) + ); diff --git a/packages/components/src/anchor/index.ts b/packages/components/src/anchor/index.ts index c02e29df..5f4c04ab 100644 --- a/packages/components/src/anchor/index.ts +++ b/packages/components/src/anchor/index.ts @@ -1,31 +1,86 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - +import { attr } from "@microsoft/fast-element"; import { - Anchor as FoundationAnchor, - anchorTemplate as template -} from '@microsoft/fast-foundation'; -import { Anchor, anchorStyles as styles } from '@microsoft/fast-components'; + Anchor as FoundationAnchor, + anchorTemplate as template, +} from "@microsoft/fast-foundation"; +import { ButtonAppearance } from "../button/index.js"; +import { anchorStyles as styles } from "./anchor.styles.js"; /** - * A function that returns a Anchor registration for configuration with a DesignSystem. - * Implements {@link @microsoft/fast-foundation#anchorTemplate} - * + * Types of anchor appearance. * @public - * @remarks - * Generates HTML Element: `` */ -export const jpAnchor = Anchor.compose({ - baseName: 'anchor', - baseClass: FoundationAnchor, - template, - styles -}); +export type AnchorAppearance = ButtonAppearance | "hypertext"; /** * Base class for Anchor * @public */ -export { Anchor }; +export class Anchor extends FoundationAnchor { + /** + * The appearance the anchor should have. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance?: AnchorAppearance; + public appearanceChanged( + oldValue: AnchorAppearance, + newValue: AnchorAppearance + ): void { + if (this.$fastController.isConnected) { + this.classList.remove(oldValue); + this.classList.add(newValue); + } + } + + public connectedCallback() { + super.connectedCallback(); + + if (!this.appearance) { + this.appearance = "neutral"; + } + } + + /** + * Applies 'icon-only' class when there is only an SVG in the default slot + * + * @internal + * + */ + public defaultSlottedContentChanged(oldValue, newValue): void { + const slottedElements = this.defaultSlottedContent.filter( + x => x.nodeType === Node.ELEMENT_NODE + ); + if (slottedElements.length === 1 && slottedElements[0] instanceof SVGElement) { + this.control.classList.add("icon-only"); + } else { + this.control.classList.remove("icon-only"); + } + } +} + +/** + * A function that returns a {@link @microsoft/fast-foundation#Anchor} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#anchorTemplate} + * + * + * @public + * @remarks + * Generates HTML Element: `` + * + * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus} + */ +export const fastAnchor = Anchor.compose({ + baseName: "anchor", + baseClass: FoundationAnchor, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, +}); export { styles as anchorStyles }; diff --git a/packages/components/src/anchored-region/anchored-region.styles.ts b/packages/components/src/anchored-region/anchored-region.styles.ts new file mode 100644 index 00000000..1918b4d4 --- /dev/null +++ b/packages/components/src/anchored-region/anchored-region.styles.ts @@ -0,0 +1,16 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { FoundationElementTemplate } from "@microsoft/fast-foundation"; + +/** + * Styles for AnchoredRegion + * @public + */ +export const anchoredRegionStyles: FoundationElementTemplate = ( + context, + definition +) => css` + :host { + contain: layout; + display: block; + } +`; diff --git a/packages/components/src/anchored-region/index.ts b/packages/components/src/anchored-region/index.ts index eb02bf38..81fd4c8c 100644 --- a/packages/components/src/anchored-region/index.ts +++ b/packages/components/src/anchored-region/index.ts @@ -1,11 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - AnchoredRegion, - anchoredRegionTemplate as template -} from '@microsoft/fast-foundation'; -import { anchoredRegionStyles as styles } from '@microsoft/fast-components'; + AnchoredRegion, + anchoredRegionTemplate as template, +} from "@microsoft/fast-foundation"; +import { anchoredRegionStyles as styles } from "./anchored-region.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#AnchoredRegion} registration for configuring the component with a DesignSystem. @@ -14,12 +11,12 @@ import { anchoredRegionStyles as styles } from '@microsoft/fast-components'; * * @beta * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpAnchoredRegion = AnchoredRegion.compose({ - baseName: 'anchored-region', - template, - styles +export const fastAnchoredRegion = AnchoredRegion.compose({ + baseName: "anchored-region", + template, + styles, }); /** diff --git a/packages/components/src/avatar/avatar.stories.ts b/packages/components/src/avatar/avatar.stories.ts index cf6a0067..eb461f1c 100644 --- a/packages/components/src/avatar/avatar.stories.ts +++ b/packages/components/src/avatar/avatar.stories.ts @@ -1,78 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import AvatarTemplate from "./fixtures/base.html"; +import "../badge/index.js"; +import "./index.js"; export default { - title: 'Components/Avatar', - argTypes: { - shape: { control: 'select', options: ['circle', 'square', 'default'] }, - fill: { - control: 'select', - options: ['none', 'accent-primary', 'accent-secondary'] - }, - color: { control: 'select', options: ['none', 'foo', 'bar'] }, - image: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - }, - decorators: [ - story => ` - ${story()}` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - ${ - args.image - ? '' - : 'JS' - } - `; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - shape: 'circle', - fill: 'none', - color: 'none', - image: false + title: "Avatar", }; -export const Square: StoryObj = { render: Template.bind({}) }; -Square.args = { - ...Default.args, - fill: 'accent-primary', - color: 'foo', - shape: 'square' -}; - -export const WithImage: StoryObj = { render: Template.bind({}) }; -WithImage.args = { - ...Default.args, - image: true -}; +export const Avatar = () => AvatarTemplate; diff --git a/packages/components/src/avatar/avatar.styles.ts b/packages/components/src/avatar/avatar.styles.ts index c52a60cc..8dc222a1 100644 --- a/packages/components/src/avatar/avatar.styles.ts +++ b/packages/components/src/avatar/avatar.styles.ts @@ -1,122 +1,106 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - AvatarOptions, - Badge, - display, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; + AvatarOptions, + Badge, + display, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; import { - accentFillRest, - baseHeightMultiplier, - controlCornerRadius, - density, - designUnit, - DirectionalStyleSheetBehavior, - foregroundOnAccentRest, - neutralForegroundRest, - typeRampBaseFontSize -} from '../design-tokens'; + baseHeightMultiplier, + controlCornerRadius, + density, + designUnit, + neutralForegroundRest, + typeRampBaseFontSize, +} from "../design-tokens.js"; +import { DirectionalStyleSheetBehavior } from "../styles/direction.js"; -const rtl: FoundationElementTemplate = ( - context, - definition -) => css` - ::slotted(${context.tagFor(Badge)}) { - left: 0; - } +const rtl = (context, definition) => css` + ::slotted(${context.tagFor(Badge)}) { + left: 0; + } `; -const ltr: FoundationElementTemplate = ( - context, - definition -) => css` - ::slotted(${context.tagFor(Badge)}) { - right: 0; - } +const ltr = (context, definition) => css` + ::slotted(${context.tagFor(Badge)}) { + right: 0; + } `; /** * Styles for Avatar * @public */ -export const avatarStyles: FoundationElementTemplate< - ElementStyles, - AvatarOptions -> = (context, definition) => - css` - ${display('flex')} :host { - position: relative; - height: var(--avatar-size, var(--avatar-size-default)); - width: var(--avatar-size, var(--avatar-size-default)); - --avatar-size-default: calc( - ( - (${baseHeightMultiplier} + ${density}) * ${designUnit} + - ((${designUnit} * 8) - 40) - ) * 1px - ); - --avatar-text-size: ${typeRampBaseFontSize}; - --avatar-text-ratio: ${designUnit}; - } +export const avatarStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("flex")} :host { + position: relative; + height: var(--avatar-size, var(--avatar-size-default)); + max-width: var(--avatar-size, var(--avatar-size-default)); + --avatar-size-default: calc( + ( + (${baseHeightMultiplier} + ${density}) * ${designUnit} + + ((${designUnit} * 8) - 40) + ) * 1px + ); + --avatar-text-size: ${typeRampBaseFontSize}; + --avatar-text-ratio: ${designUnit}; + } - .link { - text-decoration: none; - color: ${neutralForegroundRest}; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - min-width: 100%; - } + .link { + text-decoration: none; + color: ${neutralForegroundRest}; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + min-width: 100%; + } - .square { - border-radius: calc(${controlCornerRadius} * 1px); - min-width: 100%; - overflow: hidden; - } + .square { + border-radius: calc(${controlCornerRadius} * 1px); + min-width: 100%; + overflow: hidden; + } - .circle { - border-radius: 100%; - min-width: 100%; - overflow: hidden; - } + .circle { + border-radius: 100%; + min-width: 100%; + overflow: hidden; + } - .backplate { - position: relative; - display: flex; - background-color: ${accentFillRest}; - } + .backplate { + position: relative; + display: flex; + } - .media, - ::slotted(img) { - max-width: 100%; - position: absolute; - display: block; - } + .media, + ::slotted(img) { + max-width: 100%; + position: absolute; + display: block; + } - .content { - font-size: calc( - ( - var(--avatar-text-size) + - var(--avatar-size, var(--avatar-size-default)) - ) / var(--avatar-text-ratio) - ); - color: ${foregroundOnAccentRest}; - line-height: var(--avatar-size, var(--avatar-size-default)); - display: block; - min-height: var(--avatar-size, var(--avatar-size-default)); - } + .content { + font-size: calc( + (var(--avatar-text-size) + var(--avatar-size, var(--avatar-size-default))) / + var(--avatar-text-ratio) + ); + line-height: var(--avatar-size, var(--avatar-size-default)); + display: block; + min-height: var(--avatar-size, var(--avatar-size-default)); + } - ::slotted(${context.tagFor(Badge)}) { - position: absolute; - display: block; - } - `.withBehaviors( - new DirectionalStyleSheetBehavior( - ltr(context, definition), - rtl(context, definition) - ) - ); + ::slotted(${context.tagFor(Badge)}) { + position: absolute; + display: block; + } + `.withBehaviors( + new DirectionalStyleSheetBehavior( + ltr(context, definition), + rtl(context, definition) + ) + ); diff --git a/packages/components/src/avatar/index.ts b/packages/components/src/avatar/index.ts index b4c8bf25..38fb330b 100644 --- a/packages/components/src/avatar/index.ts +++ b/packages/components/src/avatar/index.ts @@ -1,16 +1,56 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { Avatar, imgTemplate } from '@microsoft/fast-components'; +import { attr, html, when } from "@microsoft/fast-element"; import { - Avatar as FoundationAvatar, - AvatarOptions, - avatarTemplate as template -} from '@microsoft/fast-foundation'; -import { avatarStyles as styles } from './avatar.styles'; + AvatarOptions, + Avatar as FoundationAvatar, + avatarTemplate as template, +} from "@microsoft/fast-foundation"; +import { avatarStyles as styles } from "./avatar.styles.js"; -export { Avatar, imgTemplate } from '@microsoft/fast-components'; -export { styles as avatarStyles }; +/** + * The FAST Avatar Class + * @public + * + */ +export class Avatar extends FoundationAvatar { + /** + * Indicates the Avatar should have an image source + * + * @public + * @remarks + * HTML Attribute: src + */ + @attr({ attribute: "src" }) + public imgSrc: string | undefined; + + /** + * Indicates the Avatar should have alt text + * + * @public + * @remarks + * HTML Attribute: alt + */ + @attr public alt: string | undefined; +} + +/** + * The FAST Avatar Template for Images + * @public + * + */ +export const imgTemplate = html` + ${when( + x => x.imgSrc, + html` + ${x => x.alt} + ` + )} +`; /** * A function that returns a {@link @microsoft/fast-foundation#Avatar} registration for configuring the component with a DesignSystem. @@ -19,15 +59,17 @@ export { styles as avatarStyles }; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpAvatar = Avatar.compose({ - baseName: 'avatar', - baseClass: FoundationAvatar, - template, - styles, - media: imgTemplate, - shadowOptions: { - delegatesFocus: true - } +export const fastAvatar = Avatar.compose({ + baseName: "avatar", + baseClass: FoundationAvatar, + template, + styles, + media: imgTemplate, + shadowOptions: { + delegatesFocus: true, + }, }); + +export { styles as avatarStyles }; diff --git a/packages/components/src/badge/badge.stories.ts b/packages/components/src/badge/badge.stories.ts index 93a16016..d1670f85 100644 --- a/packages/components/src/badge/badge.stories.ts +++ b/packages/components/src/badge/badge.stories.ts @@ -1,63 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import BadgeTemplate from "./fixtures/base.html"; +import "./index.js"; export default { - title: 'Components/Badge', - argTypes: { - circular: { control: 'boolean' }, - fill: { - control: 'select', - options: ['none', 'accent-primary', 'accent-secondary'] - }, - color: { control: 'select', options: ['none', 'foo', 'bar'] } - }, - parameters: { - actions: { - disabled: true - } - }, - decorators: [ - story => ` - ${story()}` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - 42 - `; + title: "Badge", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - circular: true, - fill: 'none', - color: 'none' -}; - -export const Square: StoryObj = { render: Template.bind({}) }; -Square.args = { - ...Default.args, - circular: false, - fill: 'accent-primary', - color: 'foo' -}; +export const Badge = () => BadgeTemplate; diff --git a/packages/components/src/badge/badge.styles.ts b/packages/components/src/badge/badge.styles.ts index f21104cb..18a483f4 100644 --- a/packages/components/src/badge/badge.styles.ts +++ b/packages/components/src/badge/badge.styles.ts @@ -1,58 +1,53 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. -import { css, ElementStyles } from '@microsoft/fast-element'; -import { display, FoundationElementTemplate } from '@microsoft/fast-foundation'; +import { css, ElementStyles } from "@microsoft/fast-element"; +import { display, FoundationElementTemplate } from "@microsoft/fast-foundation"; import { - bodyFont, - controlCornerRadius, - designUnit, - neutralFillRest, - neutralForegroundRest, - strokeWidth, - typeRampMinus1FontSize, - typeRampMinus1LineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentForegroundRest, + bodyFont, + controlCornerRadius, + designUnit, + strokeWidth, + typeRampMinus1FontSize, + typeRampMinus1LineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Badge * @public */ export const badgeStyles: FoundationElementTemplate = ( - context, - definition -) => css` - ${display('inline-block')} :host { - box-sizing: border-box; - font-family: ${bodyFont}; - font-size: ${typeRampMinus1FontSize}; - line-height: ${typeRampMinus1LineHeight}; - } + context, + definition +) => + css` + ${display("inline-block")} :host { + box-sizing: border-box; + font-family: ${bodyFont}; + font-size: ${typeRampMinus1FontSize}; + line-height: ${typeRampMinus1LineHeight}; + } - .control { - border-radius: calc(${controlCornerRadius} * 1px); - padding: calc(((${designUnit} * 0.5) - ${strokeWidth}) * 1px) - calc((${designUnit} - ${strokeWidth}) * 1px); - color: ${neutralForegroundRest}; - font-weight: 600; - border: calc(${strokeWidth} * 1px) solid transparent; - background-color: ${neutralFillRest}; - } + .control { + border-radius: calc(${controlCornerRadius} * 1px); + padding: calc(((${designUnit} * 0.5) - ${strokeWidth}) * 1px) + calc((${designUnit} - ${strokeWidth}) * 1px); + color: ${accentForegroundRest}; + font-weight: 600; + border: calc(${strokeWidth} * 1px) solid transparent; + } - .control[style] { - font-weight: 400; - } + .control[style] { + font-weight: 400; + } - :host([circular]) .control { - border-radius: 100px; - padding: 0 calc(${designUnit} * 1px); - /* Need to work with Brian on width and height here */ - height: calc((${heightNumber} - (${designUnit} * 3)) * 1px); - min-width: calc((${heightNumber} - (${designUnit} * 3)) * 1px); - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; - } -`; + :host([circular]) .control { + border-radius: 100px; + padding: 0 calc(${designUnit} * 1px); + height: calc((${heightNumber} - (${designUnit} * 3)) * 1px); + min-width: calc((${heightNumber} - (${designUnit} * 3)) * 1px); + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + } + `; diff --git a/packages/components/src/badge/index.ts b/packages/components/src/badge/index.ts index 3cebc7ca..c2a8d7c5 100644 --- a/packages/components/src/badge/index.ts +++ b/packages/components/src/badge/index.ts @@ -1,8 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { Badge, badgeTemplate as template } from '@microsoft/fast-foundation'; -import { badgeStyles as styles } from './badge.styles'; +import { Badge, badgeTemplate as template } from "@microsoft/fast-foundation"; +import { badgeStyles as styles } from "./badge.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Badge} registration for configuring the component with a DesignSystem. @@ -11,12 +8,12 @@ import { badgeStyles as styles } from './badge.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpBadge = Badge.compose({ - baseName: 'badge', - template, - styles +export const fastBadge = Badge.compose({ + baseName: "badge", + template, + styles, }); /** diff --git a/packages/components/src/breadcrumb-item/breadcrumb-item.stories.ts b/packages/components/src/breadcrumb-item/breadcrumb-item.stories.ts index 1c7e96ce..5cfdf0ae 100644 --- a/packages/components/src/breadcrumb-item/breadcrumb-item.stories.ts +++ b/packages/components/src/breadcrumb-item/breadcrumb-item.stories.ts @@ -1,59 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import BreadcrumbItemTemplate from "./fixtures/base.html"; +import "./index.js"; export default { - title: 'Components/Breadcrumb Item', - argTypes: { - href: { control: 'boolean' }, - startIcon: { control: 'boolean' }, - endIcon: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - ${args.startIcon ? getFaIcon('folder', 'start') : ''} - Breadcrumb item - ${args.endIcon ? getFaIcon('robot', 'end') : ''} - `; + title: "Breadcrumb Item", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - href: true, - startIcon: false, - endIcon: false -}; - -export const WithoutHref: StoryObj = { render: Template.bind({}) }; -WithoutHref.args = { - ...Default.args, - href: false -}; - -export const WithStartIcon: StoryObj = { render: Template.bind({}) }; -WithStartIcon.args = { - ...Default.args, - startIcon: true -}; - -export const WithEndIcon: StoryObj = { render: Template.bind({}) }; -WithEndIcon.args = { - ...Default.args, - endIcon: true -}; +export const BreadcrumbItem = () => BreadcrumbItemTemplate; diff --git a/packages/components/src/breadcrumb-item/breadcrumb-item.styles.ts b/packages/components/src/breadcrumb-item/breadcrumb-item.styles.ts index 4c7e194b..d04952c7 100644 --- a/packages/components/src/breadcrumb-item/breadcrumb-item.styles.ts +++ b/packages/components/src/breadcrumb-item/breadcrumb-item.styles.ts @@ -1,40 +1,35 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - BreadcrumbItemOptions, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + BreadcrumbItemOptions, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentForegroundActive, - accentForegroundFocus, - accentForegroundHover, - accentForegroundRest, - bodyFont, - focusStrokeWidth, - neutralForegroundRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentForegroundActive, + accentForegroundHover, + accentForegroundRest, + bodyFont, + focusStrokeWidth, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Breadcrumb item * @public */ export const breadcrumbItemStyles: FoundationElementTemplate< - ElementStyles, - BreadcrumbItemOptions + ElementStyles, + BreadcrumbItemOptions > = (context, definition) => - css` - ${display('inline-flex')} :host { + css` + ${display("inline-flex")} :host { background: transparent; box-sizing: border-box; font-family: ${bodyFont}; @@ -101,7 +96,7 @@ export const breadcrumbItemStyles: FoundationElementTemplate< } .control:${focusVisible} .content::before { - background: ${accentForegroundFocus}; + background: ${neutralForegroundRest}; height: calc(${focusStrokeWidth} * 1px); } @@ -133,14 +128,16 @@ export const breadcrumbItemStyles: FoundationElementTemplate< margin-inline-start: 6px; } `.withBehaviors( - forcedColorsStylesheetBehavior(css` - .control:hover .content::before, + forcedColorsStylesheetBehavior( + css` + .control:hover .content::before, .control:${focusVisible} .content::before { - background: ${SystemColors.LinkText}; - } - .start, - .end { - fill: ${SystemColors.ButtonText}; - } - `) - ); + background: ${SystemColors.LinkText}; + } + .start, + .end { + fill: ${SystemColors.ButtonText}; + } + ` + ) + ); diff --git a/packages/components/src/breadcrumb-item/index.ts b/packages/components/src/breadcrumb-item/index.ts index 2ebbe580..dd241592 100644 --- a/packages/components/src/breadcrumb-item/index.ts +++ b/packages/components/src/breadcrumb-item/index.ts @@ -1,30 +1,27 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - BreadcrumbItem, - BreadcrumbItemOptions, - breadcrumbItemTemplate as template -} from '@microsoft/fast-foundation'; -import { breadcrumbItemStyles as styles } from './breadcrumb-item.styles'; + BreadcrumbItem, + BreadcrumbItemOptions, + breadcrumbItemTemplate as template, +} from "@microsoft/fast-foundation"; +import { breadcrumbItemStyles as styles } from "./breadcrumb-item.styles.js"; /** - * A function that returns a BreadcrumbItem registration for configuring the component with a DesignSystem. + * A function that returns a {@link @microsoft/fast-foundation#BreadcrumbItem} registration for configuring the component with a DesignSystem. * Implements {@link @microsoft/fast-foundation#breadcrumbItemTemplate} * * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpBreadcrumbItem = BreadcrumbItem.compose({ - baseName: 'breadcrumb-item', - template, - styles, - separator: '/', - shadowOptions: { - delegatesFocus: true - } +export const fastBreadcrumbItem = BreadcrumbItem.compose({ + baseName: "breadcrumb-item", + template, + styles, + separator: "/", + shadowOptions: { + delegatesFocus: true, + }, }); /** diff --git a/packages/components/src/breadcrumb/breadcrumb.stories.ts b/packages/components/src/breadcrumb/breadcrumb.stories.ts index 569a1377..b77871c8 100644 --- a/packages/components/src/breadcrumb/breadcrumb.stories.ts +++ b/packages/components/src/breadcrumb/breadcrumb.stories.ts @@ -1,81 +1,29 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import { STORY_RENDERED } from "@storybook/core-events"; +import addons from "@storybook/addons"; +import BreadcrumbTemplate from "./fixtures/base.html"; -export default { - title: 'Components/Breadcrumb', - argTypes: { - customChildren: { control: 'boolean' }, - svgSeparator: { control: 'boolean' }, - startIcon: { control: 'boolean' }, - endIcon: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); +function addItem(): void { + const breadcrumbElement = document.getElementById("mybreadcrumb"); + const items = breadcrumbElement?.querySelectorAll("fast-breadcrumb-item"); - return ` - ${[1, 2, 3] - .map(v => - args.customChildren - ? ` - ${args.startIcon ? getFaIcon('folder', 'start') : ''} - - Breadcrumb item ${v} - - ${args.endIcon ? getFaIcon('robot', 'end') : ''} - ${args.svgSeparator ? getFaIcon('angle-right', 'separator') : ''} - ` - : ` - ${args.startIcon ? getFaIcon('folder', 'start') : ''} - Breadcrumb item ${v} - ${args.endIcon ? getFaIcon('robot', 'end') : ''} - ${args.svgSeparator ? getFaIcon('angle-right', 'separator') : ''} - ` - ) - .join('\n')} - `; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - customChildren: false, - svgSeparator: false, - startIcon: false, - endIcon: false -}; + if (items !== undefined) { + const item: any = document.createElement("fast-breadcrumb-item"); + item.setAttribute("href", "#"); + item.textContent = `Breadcrumb item ${items.length + 1}`; -export const WithCustomChildren: StoryObj = { render: Template.bind({}) }; -WithCustomChildren.args = { - ...Default.args, - customChildren: true -}; + breadcrumbElement?.appendChild(item); + } +} -export const WithSvgSeparator: StoryObj = { render: Template.bind({}) }; -WithSvgSeparator.args = { - ...Default.args, - svgSeparator: true -}; +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase() === "breadcrumb--breadcrumb") { + const button = document.getElementById("add-button") as HTMLElement; + button.addEventListener("click", () => addItem()); + } +}); -export const WithStartIcon: StoryObj = { render: Template.bind({}) }; -WithStartIcon.args = { - ...Default.args, - startIcon: true +export default { + title: "Breadcrumb", }; -export const WithEndIcon: StoryObj = { render: Template.bind({}) }; -WithEndIcon.args = { - ...Default.args, - endIcon: true -}; +export const Breadcrumb = () => BreadcrumbTemplate; diff --git a/packages/components/src/breadcrumb/index.ts b/packages/components/src/breadcrumb/index.ts index 38219862..e4cb4685 100644 --- a/packages/components/src/breadcrumb/index.ts +++ b/packages/components/src/breadcrumb/index.ts @@ -1,25 +1,19 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - Breadcrumb, - breadcrumbTemplate as template -} from '@microsoft/fast-foundation'; -import { breadcrumbStyles as styles } from '@microsoft/fast-components'; +import { Breadcrumb, breadcrumbTemplate as template } from "@microsoft/fast-foundation"; +import { breadcrumbStyles as styles } from "./breadcrumb.styles.js"; /** - * A function that returns a Breadcrumb registration for configuring the component with a DesignSystem. + * A function that returns a {@link @microsoft/fast-foundation#Breadcrumb} registration for configuring the component with a DesignSystem. * Implements {@link @microsoft/fast-foundation#breadcrumbTemplate} * * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpBreadcrumb = Breadcrumb.compose({ - baseName: 'breadcrumb', - template, - styles +export const fastBreadcrumb = Breadcrumb.compose({ + baseName: "breadcrumb", + template, + styles, }); /** diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md deleted file mode 100644 index df0ba887..00000000 --- a/packages/components/src/button/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Jupyter Button - -The `jp-button` is a web component implementation of a [button element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button). The `jp-button` also supports several visual appearances; primary, secondary, and icon. - -## Attributes - -| Attribute | Type | Description | -| ---------------- | ------- | --------------------------------------------------------------------------------------- | -| `appearance` | string | Determines the visual appearance _(primary, secondary, icon)_ of the button. | -| `aria-label` | string | Defines a label for buttons that screen readers can use. | -| `autofocus` | boolean | Determines if the element should receive document focus on page load. | -| `disabled` | boolean | Prevents the user from interacting with the button––it cannot be pressed or focused. | -| `minimal` | boolean | Compact layout | -| `form` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `formaction` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `formenctype` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `formmethod` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `formnovalidate` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `formtarget` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `name` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `type` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | -| `value` | string | See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attributes). | - -## Usage - -### Basic Usage - -[Interactive Storybook Example](https://jupyterlab-contrib.github.io/jupyter-ui-toolkit/?path=/story/library-button--default) - -```html -Button Text -``` - -### Appearance Attribute - -There are a number of visual appearances that the `jp-button` can have. The default appearance is `primary`. - -[Interactive Storybook Example](https://jupyterlab-contrib.github.io/jupyter-ui-toolkit/?path=/story/library-button--default) - -```html -Button Text -Button Text - - - -``` - -### Autofocus Attribute - -[Interactive Storybook Example](https://jupyterlab-contrib.github.io/jupyter-ui-toolkit/?path=/story/library-button--with-autofocus) - -```html -Button Text -``` - -### Disabled Attribute - -[Interactive Storybook Example](https://jupyterlab-contrib.github.io/jupyter-ui-toolkit/?path=/story/library-button--with-disabled) - -```html -Button Text -``` - -### Start Icon - -An icon can be added to the left of Button text by adding an element with the attribute `slot="start"`. - -[Interactive Storybook Example](https://jupyterlab-contrib.github.io/jupyter-ui-toolkit/?path=/story/library-button--with-start-icon) - -```html - - - - Button Text - - -``` - -### Icon Only - -An icon can also fill the default slot of the Button component (instead of text) to create an icon button by using the `appearance="icon"` attribute and value. - -**❗️❗️❗️ Important ❗️❗️❗️** - -Because icon buttons do not have text that can be used by screen readers, they are not meaningfully/semantically accessible by default. - -An `aria-label` of "Icon Button" is automatically defined on all icon buttons so they are still technically accessible out of the box, however, a descriptive and meaningful label that fits the use case or context of the icon button should be defined to replace the default label. - -For example, if you're using an icon button to confirm a state change, adding an `aria-label` with the value "Confirm" or "Confirm Changes" would be appropriate. - -[Interactive Storybook Example](https://jupyterlab-contrib.github.io/jupyter-ui-toolkit/?path=/story/library-button--with-icon-only) - -```html - - - - - -``` diff --git a/packages/components/src/button/button.open-ui.definition.ts b/packages/components/src/button/button.open-ui.definition.ts new file mode 100644 index 00000000..edf05757 --- /dev/null +++ b/packages/components/src/button/button.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Button", + url: "https://fast.design/docs/components/button", +}; diff --git a/packages/components/src/button/button.pw.spec.ts b/packages/components/src/button/button.pw.spec.ts new file mode 100644 index 00000000..40b45d90 --- /dev/null +++ b/packages/components/src/button/button.pw.spec.ts @@ -0,0 +1,52 @@ +import type { + Button as FASTButtonType +} from "@microsoft/fast-foundation"; +import chai from "chai"; + +const { expect } = chai; + +type FASTButton = HTMLElement & FASTButtonType; + +describe("FASTButton", function () { + beforeEach(async function () { + if (!this.page && !this.browser) { + this.skip(); + } + + this.documentHandle = await this.page.evaluateHandle(() => document); + + this.setupHandle = await this.page.evaluateHandle( + (document) => { + const element = document.createElement("fast-button") as FASTButton; + element.textContent = "Hello"; + element.id = "Button1"; + + document.body.appendChild(element) + }, + this.documentHandle + ); + }); + + afterEach(async function () { + if (this.setupHandle) { + await this.setupHandle.dispose(); + } + }); + + // FASTButton should render on the page + it("should render on the page", async function () { + const element = await this.page.waitForSelector("fast-button"); + + expect(element).to.exist; + }); + + it("receive focus when focused programatically", async function () { + const element = await this.page.waitForSelector("fast-button"); + + await this.page.evaluateHandle(element => element.focus(), element) + + expect(await this.page.evaluate( + () => document.activeElement?.id + )).to.equal(await element.evaluate(node => node.id)); + }); +}); diff --git a/packages/components/src/button/button.stories.ts b/packages/components/src/button/button.stories.ts index 92a24d3d..9ce5e6f8 100644 --- a/packages/components/src/button/button.stories.ts +++ b/packages/components/src/button/button.stories.ts @@ -1,126 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import ButtonTemplate from "./fixtures/button.html"; +import "./index.js"; export default { - title: 'Components/Button', - argTypes: { - label: { control: 'text' }, - appearance: { - control: 'select', - options: [ - 'Accent', - 'Error', - 'Lightweight', - 'Neutral', - 'Outline', - 'Stealth' - ] - }, - isDisabled: { control: 'boolean' }, - isAutoFocused: { control: 'boolean' }, - isMinimal: { control: 'boolean' }, - startIcon: { control: 'boolean' }, - onClick: { - action: 'clicked', - table: { - disable: true - } - }, - ariaPressed: { - control: 'select', - options: ['none', 'true', 'false'] - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - `${args.startIcon ? getFaIcon('plus', args.label ? 'start' : null) : ''}${ - args.label ?? '' - }` - ); - - if (args.onClick) { - container.firstChild!.addEventListener('click', args.onClick); - } - return container.firstChild as HTMLElement; -}; - -export const Accent: StoryObj = { render: Template.bind({}) }; -Accent.args = { - label: 'Button Text', - appearance: 'Accent', - isDisabled: false, - isAutoFocused: false, - isMinimal: false, - startIcon: false, - ariaPressed: 'none', - onClick: action('button-clicked') -}; - -export const Error: StoryObj = { render: Template.bind({}) }; -Error.args = { - ...Accent.args, - appearance: 'Error' -}; - -export const Neutral: StoryObj = { render: Template.bind({}) }; -Neutral.args = { - ...Accent.args, - appearance: 'Neutral' -}; - -export const Lightweight: StoryObj = { render: Template.bind({}) }; -Lightweight.args = { - ...Accent.args, - appearance: 'Lightweight' + title: "Button", }; -export const WithAutofocus: StoryObj = { render: Template.bind({}) }; -WithAutofocus.args = { - ...Accent.args, - isAutoFocused: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Accent.args, - isDisabled: true -}; - -export const WithStartIcon: StoryObj = { render: Template.bind({}) }; -WithStartIcon.args = { - ...Accent.args, - startIcon: true -}; - -export const IconOnly: StoryObj = { render: Template.bind({}) }; -IconOnly.args = { - ...Accent.args, - label: null, - startIcon: true -}; - -export const Toggle: StoryObj = { render: Template.bind({}) }; -Toggle.storyName = 'Toggle button'; -Toggle.args = { - ...Accent.args, - ariaPressed: 'true' -}; +export const Button = () => ButtonTemplate; diff --git a/packages/components/src/button/button.styles.ts b/packages/components/src/button/button.styles.ts index 5410ed9f..fed9e64b 100644 --- a/packages/components/src/button/button.styles.ts +++ b/packages/components/src/button/button.styles.ts @@ -1,679 +1,162 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { neutralFillStrongActive } from '@microsoft/fast-components'; -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - ButtonOptions, - disabledCursor, - display, - ElementDefinitionContext, - focusVisible, - forcedColorsStylesheetBehavior, - PropertyStyleSheetBehavior -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + ButtonOptions, + disabledCursor, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillActive, - accentFillFocus, - accentFillHover, - accentFillRest, - accentForegroundActive, - accentForegroundHover, - accentForegroundRest, - bodyFont, - controlCornerRadius, - density, - designUnit, - disabledOpacity, - errorFillActive, - errorFillFocus, - errorFillHover, - errorFillRest, - errorForegroundActive, - focusStrokeWidth, - foregroundOnAccentActive, - foregroundOnAccentHover, - foregroundOnAccentRest, - neutralFillActive, - neutralFillHover, - neutralFillRest, - neutralFillStealthActive, - neutralFillStealthHover, - neutralFillStealthRest, - neutralFillStrongFocus, - neutralForegroundRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles'; - -/** - * Behavior that will conditionally apply a stylesheet based on the elements - * appearance property - * - * @param value - The value of the appearance property - * @param styles - The styles to be applied when condition matches - * - * @internal - */ -function appearanceBehavior(value: string, styles: ElementStyles) { - return new PropertyStyleSheetBehavior('appearance', value, styles); -} - -// TODO do we really want to use outline for focus => this call for a minimal style for toolbar probably -// outline force to use a margin so that the outline is not hidden by other elements. - -/** - * @internal - */ -const BaseButtonStyles = css` - ${display('inline-flex')} :host { - font-family: ${bodyFont}; - outline: none; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - height: calc(${heightNumber} * 1px); - min-width: calc(${heightNumber} * 1px); - background-color: ${neutralFillRest}; - color: ${neutralForegroundRest}; - border-radius: calc(${controlCornerRadius} * 1px); - fill: currentcolor; - cursor: pointer; - margin: calc((${focusStrokeWidth} + 2) * 1px); - } - - .control { - background: transparent; - height: inherit; - flex-grow: 1; - box-sizing: border-box; - display: inline-flex; - justify-content: center; - align-items: center; - padding: 0 calc((10 + (${designUnit} * 2 * ${density})) * 1px); - white-space: nowrap; - outline: none; - text-decoration: none; - border: calc(${strokeWidth} * 1px) solid transparent; - color: inherit; - border-radius: inherit; - fill: inherit; - cursor: inherit; - font-family: inherit; - font-size: inherit; - line-height: inherit; - } - - :host(:hover) { - background-color: ${neutralFillHover}; - } - - :host(:active) { - background-color: ${neutralFillActive}; - } - - :host([aria-pressed='true']) { - box-shadow: inset 0px 0px 2px 2px ${neutralFillStrongActive}; - } - - :host([minimal]) { - --density: -4; - } - - :host([minimal]) .control { - padding: 1px; - } - - /* prettier-ignore */ - .control:${focusVisible} { - outline: calc(${focusStrokeWidth} * 1px) solid ${neutralFillStrongFocus}; - outline-offset: 2px; - -moz-outline-radius: 0px; - } - - .control::-moz-focus-inner { - border: 0; - } - - .start, - .end { - display: flex; - } - - .control.icon-only { - padding: 0; - line-height: 0; - } - - ::slotted(svg) { - ${ - /* Glyph size and margin-left is temporary - - replace when adaptive typography is figured out */ '' - } width: 16px; - height: 16px; - pointer-events: none; - } - - .start { - margin-inline-end: 11px; - } - - .end { - margin-inline-start: 11px; - } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host .control { - background-color: ${SystemColors.ButtonFace}; - border-color: ${SystemColors.ButtonText}; - color: ${SystemColors.ButtonText}; - fill: currentColor; - } - - :host(:hover) .control { - forced-color-adjust: none; - background-color: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } - - /* prettier-ignore */ - .control:${focusVisible} { - forced-color-adjust: none; - background-color: ${SystemColors.Highlight}; - outline-color: ${SystemColors.ButtonText}; - color: ${SystemColors.HighlightText}; - } - - .control:hover, - :host([appearance='outline']) .control:hover { - border-color: ${SystemColors.ButtonText}; - } - - :host([href]) .control { - border-color: ${SystemColors.LinkText}; - color: ${SystemColors.LinkText}; - } - - :host([href]) .control:hover, - :host([href]) .control:${focusVisible} { - forced-color-adjust: none; - background: ${SystemColors.ButtonFace}; - outline-color: ${SystemColors.LinkText}; - color: ${SystemColors.LinkText}; - fill: currentColor; - } - `) -); - -/** - * @internal - */ -const AccentButtonStyles = css` - :host([appearance='accent']) { - background: ${accentFillRest}; - color: ${foregroundOnAccentRest}; - } - - :host([appearance='accent']:hover) { - background: ${accentFillHover}; - color: ${foregroundOnAccentHover}; - } - - :host([appearance='accent'][aria-pressed='true']) { - box-shadow: inset 0px 0px 2px 2px ${accentForegroundActive}; - } - - :host([appearance='accent']:active) .control:active { - background: ${accentFillActive}; - color: ${foregroundOnAccentActive}; - } - - :host([appearance="accent"]) .control:${focusVisible} { - outline-color: ${accentFillFocus}; - } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='accent']) .control { - forced-color-adjust: none; - background: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } - - :host([appearance='accent']) .control:hover, - :host([appearance='accent']:active) .control:active { - background: ${SystemColors.HighlightText}; - border-color: ${SystemColors.Highlight}; - color: ${SystemColors.Highlight}; - } - - :host([appearance="accent"]) .control:${focusVisible} { - outline-color: ${SystemColors.Highlight}; - } - - :host([appearance='accent'][href]) .control { - background: ${SystemColors.LinkText}; - color: ${SystemColors.HighlightText}; - } - - :host([appearance='accent'][href]) .control:hover { - background: ${SystemColors.ButtonFace}; - border-color: ${SystemColors.LinkText}; - box-shadow: none; - color: ${SystemColors.LinkText}; - fill: currentColor; - } - - :host([appearance="accent"][href]) .control:${focusVisible} { - outline-color: ${SystemColors.HighlightText}; - } - `) -); - -/** - * @internal - */ -const ErrorButtonStyles = css` - :host([appearance='error']) { - background: ${errorFillRest}; - color: ${foregroundOnAccentRest}; - } - - :host([appearance='error']:hover) { - background: ${errorFillHover}; - color: ${foregroundOnAccentHover}; - } - - :host([appearance='error'][aria-pressed='true']) { - box-shadow: inset 0px 0px 2px 2px ${errorForegroundActive}; - } - - :host([appearance='error']:active) .control:active { - background: ${errorFillActive}; - color: ${foregroundOnAccentActive}; - } - - :host([appearance="error"]) .control:${focusVisible} { - outline-color: ${errorFillFocus}; - } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='error']) .control { - forced-color-adjust: none; - background: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } - - :host([appearance='error']) .control:hover, - :host([appearance='error']:active) .control:active { - background: ${SystemColors.HighlightText}; - border-color: ${SystemColors.Highlight}; - color: ${SystemColors.Highlight}; - } - - :host([appearance="error"]) .control:${focusVisible} { - outline-color: ${SystemColors.Highlight}; - } - - :host([appearance='error'][href]) .control { - background: ${SystemColors.LinkText}; - color: ${SystemColors.HighlightText}; - } - - :host([appearance='error'][href]) .control:hover { - background: ${SystemColors.ButtonFace}; - border-color: ${SystemColors.LinkText}; - box-shadow: none; - color: ${SystemColors.LinkText}; - fill: currentColor; - } - - :host([appearance="error"][href]) .control:${focusVisible} { - outline-color: ${SystemColors.HighlightText}; - } - `) -); - -/** - * @internal - */ -export const LightweightButtonStyles = css` - :host([appearance='lightweight']) { - background: transparent; - color: ${accentForegroundRest}; - } - - :host([appearance='lightweight']) .control { - padding: 0; - height: initial; - border: none; - box-shadow: none; - border-radius: 0; - } - - :host([appearance='lightweight']:hover) { - background: transparent; - color: ${accentForegroundHover}; - } - - :host([appearance='lightweight']:active) { - background: transparent; - color: ${accentForegroundActive}; - } - - :host([appearance='lightweight']) .content { - position: relative; - } - - :host([appearance='lightweight']) .content::before { - content: ''; - display: block; - height: calc(${strokeWidth} * 1px); - position: absolute; - top: calc(1em + 4px); - width: 100%; - } - - :host([appearance='lightweight']:hover) .content::before { - background: ${accentForegroundHover}; - } - - :host([appearance='lightweight']:active) .content::before { - background: ${accentForegroundActive}; - } - - :host([appearance="lightweight"]) .control:${focusVisible} { - outline-color: transparent; - } - - :host([appearance="lightweight"]) .control:${focusVisible} .content::before { - background: ${neutralForegroundRest}; - height: calc(${focusStrokeWidth} * 1px); - } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance="lightweight"]) .control:hover, - :host([appearance="lightweight"]) .control:${focusVisible} { - forced-color-adjust: none; - background: ${SystemColors.ButtonFace}; - color: ${SystemColors.Highlight}; - } - :host([appearance="lightweight"]) .control:hover .content::before, - :host([appearance="lightweight"]) .control:${focusVisible} .content::before { - background: ${SystemColors.Highlight}; - } - - :host([appearance="lightweight"][href]) .control:hover, - :host([appearance="lightweight"][href]) .control:${focusVisible} { - background: ${SystemColors.ButtonFace}; - box-shadow: none; - color: ${SystemColors.LinkText}; - } - - :host([appearance="lightweight"][href]) .control:hover .content::before, - :host([appearance="lightweight"][href]) .control:${focusVisible} .content::before { - background: ${SystemColors.LinkText}; - } - `) -); - -/** - * @internal - */ -const OutlineButtonStyles = css` - :host([appearance='outline']) { - background: transparent; - border-color: ${accentFillRest}; - } - - :host([appearance='outline']:hover) { - border-color: ${accentFillHover}; - } - - :host([appearance='outline']:active) { - border-color: ${accentFillActive}; - } - - :host([appearance='outline']) .control { - border-color: inherit; - } - - :host([appearance="outline"]) .control:${focusVisible} { - outline-color: ${accentFillFocus}; - } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='outline']) .control { - border-color: ${SystemColors.ButtonText}; - } - :host([appearance="outline"]) .control:${focusVisible} { - forced-color-adjust: none; - background-color: ${SystemColors.Highlight}; - outline-color: ${SystemColors.ButtonText}; - color: ${SystemColors.HighlightText}; - fill: currentColor; - } - :host([appearance='outline'][href]) .control { - background: ${SystemColors.ButtonFace}; - border-color: ${SystemColors.LinkText}; - color: ${SystemColors.LinkText}; - fill: currentColor; - } - :host([appearance="outline"][href]) .control:hover, - :host([appearance="outline"][href]) .control:${focusVisible} { - forced-color-adjust: none; - outline-color: ${SystemColors.LinkText}; - } - `) -); - -/** - * @internal - */ -const StealthButtonStyles = css` - :host([appearance='stealth']) { - background: transparent; - } - - :host([appearance='stealth']:hover) { - background: ${neutralFillStealthHover}; - } - - :host([appearance='stealth']:active) { - background: ${neutralFillStealthActive}; - } - - :host([appearance='stealth']) .control:${focusVisible} { - outline-color: ${accentFillFocus}; - } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='stealth']), - :host([appearance='stealth']) .control { - forced-color-adjust: none; - background: ${SystemColors.ButtonFace}; - border-color: transparent; - color: ${SystemColors.ButtonText}; - fill: currentColor; - } - - :host([appearance='stealth']:hover) .control { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - fill: currentColor; - } - - :host([appearance="stealth"]:${focusVisible}) .control { - outline-color: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - fill: currentColor; - } - - :host([appearance='stealth'][href]) .control { - color: ${SystemColors.LinkText}; - } - - :host([appearance="stealth"][href]:hover) .control, - :host([appearance="stealth"][href]:${focusVisible}) .control { - background: ${SystemColors.LinkText}; - border-color: ${SystemColors.LinkText}; - color: ${SystemColors.HighlightText}; - fill: currentColor; - } - - :host([appearance="stealth"][href]:${focusVisible}) .control { - forced-color-adjust: none; - box-shadow: 0 0 0 1px ${SystemColors.LinkText}; - } - `) -); + accentFillRest, + accentForegroundRest, + disabledOpacity, + neutralFillRest, + neutralFillStealthRest, +} from "../design-tokens.js"; +import { + AccentButtonStyles, + BaseButtonStyles, + LightweightButtonStyles, + OutlineButtonStyles, + StealthButtonStyles, +} from "../styles/index.js"; +import { appearanceBehavior } from "../utilities/behaviors.js"; /** * Styles for Button * @public */ -export const buttonStyles: ( - context: ElementDefinitionContext, - definition: ButtonOptions -) => ElementStyles = ( - context: ElementDefinitionContext, - definition: ButtonOptions +export const buttonStyles: FoundationElementTemplate = ( + context, + definition ) => - css` - :host([disabled]), - :host([disabled]:hover), - :host([disabled]:active) { - opacity: ${disabledOpacity}; - background-color: ${neutralFillRest}; - cursor: ${disabledCursor}; - } - - ${BaseButtonStyles} - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([disabled]), - :host([disabled]) .control, - :host([disabled]:hover), - :host([disabled]:active) { - forced-color-adjust: none; - background-color: ${SystemColors.ButtonFace}; - outline-color: ${SystemColors.GrayText}; - color: ${SystemColors.GrayText}; - cursor: ${disabledCursor}; - opacity: 1; - } - `), - appearanceBehavior( - 'accent', - css` - :host([appearance='accent'][disabled]), - :host([appearance='accent'][disabled]:hover), - :host([appearance='accent'][disabled]:active) { - background: ${accentFillRest}; - } - - ${AccentButtonStyles} - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='accent'][disabled]) .control, - :host([appearance='accent'][disabled]) .control:hover { - background: ${SystemColors.ButtonFace}; - border-color: ${SystemColors.GrayText}; - color: ${SystemColors.GrayText}; - } - `) - ) - ), - appearanceBehavior( - 'error', - css` - :host([appearance='error'][disabled]), - :host([appearance='error'][disabled]:hover), - :host([appearance='error'][disabled]:active) { - background: ${errorFillRest}; + css` + :host([disabled]), + :host([disabled]:hover), + :host([disabled]:active) { + opacity: ${disabledOpacity}; + background-color: ${neutralFillRest}; + cursor: ${disabledCursor}; } - ${ErrorButtonStyles} - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='error'][disabled]) .control, - :host([appearance='error'][disabled]) .control:hover { - background: ${SystemColors.ButtonFace}; - border-color: ${SystemColors.GrayText}; - color: ${SystemColors.GrayText}; - } - `) - ) - ), - appearanceBehavior( - 'lightweight', - css` - :host([appearance='lightweight'][disabled]:hover), - :host([appearance='lightweight'][disabled]:active) { - background-color: transparent; - color: ${accentForegroundRest}; - } - - :host([appearance='lightweight'][disabled]) .content::before, - :host([appearance='lightweight'][disabled]:hover) .content::before, - :host([appearance='lightweight'][disabled]:active) .content::before { - background: transparent; - } - - ${LightweightButtonStyles} - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='lightweight'].disabled) .control { - forced-color-adjust: none; - color: ${SystemColors.GrayText}; - } - - :host([appearance='lightweight'].disabled) - .control:hover - .content::before { - background: none; - } - `) - ) - ), - appearanceBehavior( - 'outline', - css` - :host([appearance='outline'][disabled]), - :host([appearance='outline'][disabled]:hover), - :host([appearance='outline'][disabled]:active) { - background: transparent; - border-color: ${accentFillRest}; - } - - ${OutlineButtonStyles} - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='outline'][disabled]) .control { - border-color: ${SystemColors.GrayText}; - } - `) - ) - ), - appearanceBehavior( - 'stealth', - css` - ${StealthButtonStyles} - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([appearance='stealth'][disabled]) { - background: ${SystemColors.ButtonFace}; - } - - :host([appearance='stealth'][disabled]) .control { - background: ${SystemColors.ButtonFace}; - border-color: transparent; - color: ${SystemColors.GrayText}; - } - `) - ) - ) - ); + ${BaseButtonStyles} + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([disabled]), + :host([disabled]) .control, + :host([disabled]:hover), + :host([disabled]:active) { + forced-color-adjust: none; + background-color: ${SystemColors.ButtonFace}; + border-color: ${SystemColors.GrayText}; + color: ${SystemColors.GrayText}; + cursor: ${disabledCursor}; + opacity: 1; + } + ` + ), + appearanceBehavior( + "accent", + css` + :host([appearance="accent"][disabled]), + :host([appearance="accent"][disabled]:hover), + :host([appearance="accent"][disabled]:active) { + background: ${accentFillRest}; + } + + ${AccentButtonStyles} + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="accent"][disabled]) .control, + :host([appearance="accent"][disabled]) .control:hover { + background: ${SystemColors.ButtonFace}; + border-color: ${SystemColors.GrayText}; + color: ${SystemColors.GrayText}; + } + ` + ) + ) + ), + appearanceBehavior( + "lightweight", + css` + :host([appearance="lightweight"][disabled]:hover), + :host([appearance="lightweight"][disabled]:active) { + background-color: transparent; + color: ${accentForegroundRest}; + } + + :host([appearance="lightweight"][disabled]) .content::before, + :host([appearance="lightweight"][disabled]:hover) .content::before, + :host([appearance="lightweight"][disabled]:active) .content::before { + background: transparent; + } + + ${LightweightButtonStyles} + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="lightweight"].disabled) .control { + forced-color-adjust: none; + color: ${SystemColors.GrayText}; + } + + :host([appearance="lightweight"].disabled) + .control:hover + .content::before { + background: none; + } + ` + ) + ) + ), + appearanceBehavior( + "outline", + css` + :host([appearance="outline"][disabled]), + :host([appearance="outline"][disabled]:hover), + :host([appearance="outline"][disabled]:active) { + background: transparent; + border-color: ${accentFillRest}; + } + + ${OutlineButtonStyles} + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="outline"][disabled]) .control { + border-color: ${SystemColors.GrayText}; + } + ` + ) + ) + ), + appearanceBehavior( + "stealth", + css` + :host([appearance="stealth"][disabled]), + :host([appearance="stealth"][disabled]:hover), + :host([appearance="stealth"][disabled]:active) { + background: ${neutralFillStealthRest}; + } + + ${StealthButtonStyles} + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="stealth"][disabled]) { + background: ${SystemColors.ButtonFace}; + } + + :host([appearance="stealth"][disabled]) .control { + background: ${SystemColors.ButtonFace}; + border-color: transparent; + color: ${SystemColors.GrayText}; + } + ` + ) + ) + ) + ); diff --git a/packages/components/src/button/index.ts b/packages/components/src/button/index.ts index a2a3ad33..b42bf08e 100644 --- a/packages/components/src/button/index.ts +++ b/packages/components/src/button/index.ts @@ -1,98 +1,72 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { attr } from '@microsoft/fast-element'; +import { attr } from "@microsoft/fast-element"; import { - Button as FoundationButton, - buttonTemplate as template -} from '@microsoft/fast-foundation'; -import { buttonStyles } from './button.styles'; + Button as FoundationButton, + buttonTemplate as template, +} from "@microsoft/fast-foundation"; +import { buttonStyles as styles } from "./button.styles.js"; /** * Types of button appearance. * @public */ export type ButtonAppearance = - | 'accent' - | 'error' - | 'lightweight' - | 'neutral' - | 'outline' - | 'stealth'; + | "accent" + | "lightweight" + | "neutral" + | "outline" + | "stealth"; /** * @internal */ export class Button extends FoundationButton { - /** - * The appearance the button should have. - * - * @public - * @remarks - * HTML Attribute: appearance - */ - @attr - public appearance: ButtonAppearance; - - /** - * Whether the button has a compact layout or not. - * - * @public - * @remarks - * HTML Attribute: minimal - */ - @attr({ attribute: 'minimal', mode: 'boolean' }) - public minimal: boolean; - - public connectedCallback(): void { - super.connectedCallback(); - if (!this.appearance) { - this.appearance = 'neutral'; - } - } + /** + * The appearance the button should have. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: ButtonAppearance = "neutral"; - /** - * Applies 'icon-only' class when there is only an SVG in the default slot - * - * @public - * @remarks - */ - public defaultSlottedContentChanged( - oldValue: HTMLElement[], - newValue: HTMLElement[] - ): void { - const slottedElements = this.defaultSlottedContent.filter( - x => x.nodeType === Node.ELEMENT_NODE - ); - if ( - slottedElements.length === 1 && - (slottedElements[0] instanceof SVGElement || - slottedElements[0].classList.contains('fa') || - slottedElements[0].classList.contains('fas')) - ) { - this.control.classList.add('icon-only'); - } else { - this.control.classList.remove('icon-only'); + /** + * Applies 'icon-only' class when there is only an SVG in the default slot + * + * @public + * @remarks + */ + public defaultSlottedContentChanged(oldValue, newValue): void { + const slottedElements = this.defaultSlottedContent.filter( + x => x.nodeType === Node.ELEMENT_NODE + ); + if (slottedElements.length === 1 && slottedElements[0] instanceof SVGElement) { + this.control.classList.add("icon-only"); + } else { + this.control.classList.remove("icon-only"); + } } - } } /** - * The button component registration. + * A function that returns a {@link @microsoft/fast-foundation#Button} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#buttonTemplate} + * * * @public * @remarks - * Generated HTML Element: `` + * Generates HTML Element: `` * * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus} */ -export const jpButton = Button.compose({ - baseName: 'button', - baseClass: FoundationButton, - template, - styles: buttonStyles, - shadowOptions: { - delegatesFocus: true - } +export const fastButton = Button.compose({ + baseName: "button", + baseClass: FoundationButton, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, }); + +export { styles as buttonStyles }; diff --git a/packages/components/src/calendar/calendar.stories.ts b/packages/components/src/calendar/calendar.stories.ts new file mode 100644 index 00000000..1f8b348c --- /dev/null +++ b/packages/components/src/calendar/calendar.stories.ts @@ -0,0 +1,8 @@ +import CalendarTemplate from "./fixtures/calendar.html"; +import "./index.js"; + +export default { + title: "Calendar", +}; + +export const Calendar = () => CalendarTemplate; diff --git a/packages/components/src/calendar/calendar.styles.ts b/packages/components/src/calendar/calendar.styles.ts new file mode 100644 index 00000000..5857e18c --- /dev/null +++ b/packages/components/src/calendar/calendar.styles.ts @@ -0,0 +1,151 @@ +import { css } from "@microsoft/fast-element"; +import { + disabledCursor, + display, + forcedColorsStylesheetBehavior, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { heightNumber } from "../styles/index.js"; +import { + accentForegroundActive, + bodyFont, + designUnit, + disabledOpacity, + foregroundOnAccentActive, + neutralFillRest, + neutralForegroundRest, + typeRampBaseFontSize, + typeRampBaseLineHeight, + typeRampPlus3FontSize, + typeRampPlus3LineHeight, +} from "../design-tokens.js"; + +/** + * Styles for Calendar + * @public + */ +export const CalendarStyles = css` + ${display("block")} :host { + --cell-border: none; + --cell-height: calc(${heightNumber} * 1px); + --selected-day-outline: 1px solid ${accentForegroundActive}; + --selected-day-color: ${accentForegroundActive}; + --selected-day-background: ${neutralFillRest}; + --cell-padding: calc(${designUnit} * 1px); + --disabled-day-opacity: ${disabledOpacity}; + --inactive-day-opacity: ${disabledOpacity}; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + color: ${neutralForegroundRest}; + } + + .title { + font-size: ${typeRampPlus3FontSize}; + line-height: ${typeRampPlus3LineHeight}; + padding: var(--cell-padding); + text-align: center; + } + + .week-days, + .week { + display: grid; + grid-template-columns: repeat(7, 1fr); + border-left: var(--cell-border, none); + border-bottom: none; + padding: 0; + } + + .interact .week { + grid-gap: calc(${designUnit} * 1px); + margin-top: calc(${designUnit} * 1px); + } + + .day, + .week-day { + border-bottom: var(--cell-border); + border-right: var(--cell-border); + padding: var(--cell-padding); + } + + .week-day { + text-align: center; + border-radius: 0; + border-top: var(--cell-border); + } + + .day { + box-sizing: border-box; + vertical-align: top; + outline-offset: -1px; + line-height: var(--cell-line-height); + white-space: normal; + } + + .interact .day { + background: ${neutralFillRest}; + cursor: pointer; + } + + .day.inactive { + background: var(--inactive-day-background); + color: var(--inactive-day-color); + opacity: var(--inactive-day-opacity); + outline: var(--inactive-day-outline); + } + + .day.disabled { + background: var(--disabled-day-background); + color: var(--disabled-day-color); + cursor: ${disabledCursor}; + opacity: var(--disabled-day-opacity); + outline: var(--disabled-day-outline); + } + + .day.selected { + color: var(--selected-day-color); + background: var(--selected-day-background); + outline: var(--selected-day-outline); + } + + .date { + padding: var(--cell-padding); + text-align: center; + } + + .interact .today, + .today { + color: ${foregroundOnAccentActive}; + background: ${accentForegroundActive}; + } + + .today.inactive .date { + background: transparent; + color: inherit; + width: auto; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + --selected-day-outline: 1px solid ${SystemColors.Highlight}; + } + + .day, + .week-day { + background: ${SystemColors.Canvas}; + color: ${SystemColors.CanvasText}; + fill: currentcolor; + } + + .day.selected { + color: ${SystemColors.Highlight}; + } + + .today .date { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + ` + ) +); diff --git a/packages/components/src/calendar/index.ts b/packages/components/src/calendar/index.ts new file mode 100644 index 00000000..3e8fc60e --- /dev/null +++ b/packages/components/src/calendar/index.ts @@ -0,0 +1,30 @@ +import { + Calendar, + calendarTemplate, + CalendarTitleTemplate, +} from "@microsoft/fast-foundation"; +import { CalendarStyles as styles } from "./calendar.styles.js"; + +/** + * The FAST Calendar Element. Implements {@link @microsoft/fast-foundation#Calendar}, + * {@link @microsoft/fast-foundation#calendarTemplate} + * + * + * @public + * @remarks + * HTML Element: `` + */ +export const fastCalendar = Calendar.compose({ + baseName: "calendar", + template: calendarTemplate, + styles, + title: CalendarTitleTemplate, +}); + +/** + * Base class for fastCalendar + * @public + */ +export { Calendar }; + +export { styles as CalendarStyles }; diff --git a/packages/components/src/card/card.stories.ts b/packages/components/src/card/card.stories.ts index 963dfb7c..7fb8beca 100644 --- a/packages/components/src/card/card.stories.ts +++ b/packages/components/src/card/card.stories.ts @@ -1,41 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { neutralForegroundRest, typeRampBaseFontSize } from '../design-tokens'; -import { setTheme } from '../utilities/storybook'; +import CardTemplate from "./fixtures/card.html"; +import "./index.js"; export default { - title: 'Components/Card', - parameters: { - controls: { - disabled: true - }, - actions: { - disabled: true - } - }, - decorators: [ - story => ` - ${story()}` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - Card with text - `; + title: "Card", }; -export const Default: StoryObj = { render: Template.bind({}) }; +export const Card = () => CardTemplate; diff --git a/packages/components/src/card/card.styles.ts b/packages/components/src/card/card.styles.ts new file mode 100644 index 00000000..03c41a0a --- /dev/null +++ b/packages/components/src/card/card.styles.ts @@ -0,0 +1,41 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + display, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { controlCornerRadius, fillColor } from "../design-tokens.js"; +import { elevation } from "../styles/index.js"; + +/** + * Styles for Card + * @public + */ +export const cardStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("block")} :host { + --elevation: 4; + display: block; + contain: content; + height: var(--card-height, 100%); + width: var(--card-width, 100%); + box-sizing: border-box; + background: ${fillColor}; + border-radius: calc(${controlCornerRadius} * 1px); + ${elevation} + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + forced-color-adjust: none; + background: ${SystemColors.Canvas}; + box-shadow: 0 0 0 1px ${SystemColors.CanvasText}; + } + ` + ) + ); diff --git a/packages/components/src/card/index.ts b/packages/components/src/card/index.ts index 7ce5cc64..7c4b6659 100644 --- a/packages/components/src/card/index.ts +++ b/packages/components/src/card/index.ts @@ -1,11 +1,32 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - Card as FoundationCard, - cardTemplate as template -} from '@microsoft/fast-foundation'; -import { Card, cardStyles as styles } from '@microsoft/fast-components'; + composedParent, + Card as FoundationCard, + cardTemplate as template, +} from "@microsoft/fast-foundation"; +import { Swatch } from "../color/swatch.js"; +import { fillColor, neutralFillLayerRecipe } from "../design-tokens.js"; +import { cardStyles as styles } from "./card.styles.js"; + +/** + * @internal + */ +export class Card extends FoundationCard { + connectedCallback() { + super.connectedCallback(); + + const parent = composedParent(this); + + if (parent) { + fillColor.setValueFor( + this, + (target: HTMLElement): Swatch => + neutralFillLayerRecipe + .getValueFor(target) + .evaluate(target, fillColor.getValueFor(parent)) + ); + } + } +} /** * A function that returns a {@link @microsoft/fast-foundation#Card} registration for configuring the component with a DesignSystem. @@ -14,13 +35,13 @@ import { Card, cardStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpCard = Card.compose({ - baseName: 'card', - baseClass: FoundationCard, - template, - styles +export const fastCard = Card.compose({ + baseName: "card", + baseClass: FoundationCard, + template, + styles, }); -export { Card, styles as cardStyles }; +export { styles as cardStyles }; diff --git a/packages/components/src/checkbox/checkbox.stories.ts b/packages/components/src/checkbox/checkbox.stories.ts index c0dfad63..a5d9d36c 100644 --- a/packages/components/src/checkbox/checkbox.stories.ts +++ b/packages/components/src/checkbox/checkbox.stories.ts @@ -1,79 +1,21 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; -import { Checkbox } from './index'; - -export default { - title: 'Components/Checkbox', - argTypes: { - label: { control: 'text' }, - isChecked: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isIndeterminate: { control: 'boolean' }, - onChange: { - action: 'changed', - table: { - disable: true - } +import addons from "@storybook/addons"; +import { STORY_RENDERED } from "@storybook/core-events"; +import { Checkbox as FoundationCheckbox } from "@microsoft/fast-foundation"; +import Examples from "./fixtures/base.html"; +import "./index.js"; + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("checkbox")) { + document + .querySelectorAll(".flag-indeterminate") + .forEach((el: FoundationCheckbox) => { + el.indeterminate = true; + }); } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.label} - ` - ); - - const checkbox = container.firstChild as Checkbox; - - if (args.isIndeterminate) { - checkbox.indeterminate = true; - } - - if (args.onChange) { - checkbox.addEventListener('change', args.onChange); - } - - return checkbox; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Checkbox', - isChecked: false, - isDisabled: false, - isIndeterminate: false, - onChange: action('checkbox-onchange') -}; +}); -export const WithChecked: StoryObj = { render: Template.bind({}) }; -WithChecked.args = { - ...Default.args, - isChecked: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true +export default { + title: "Checkbox", }; -export const WithIndeterminate: StoryObj = { render: Template.bind({}) }; -WithIndeterminate.args = { - ...Default.args, - isIndeterminate: true -}; +export const Checkbox = () => Examples; diff --git a/packages/components/src/checkbox/checkbox.styles.ts b/packages/components/src/checkbox/checkbox.styles.ts index 5a1c3375..e9308fe0 100644 --- a/packages/components/src/checkbox/checkbox.styles.ts +++ b/packages/components/src/checkbox/checkbox.styles.ts @@ -1,253 +1,232 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - CheckboxOptions, - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + CheckboxOptions, + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillActive, - accentFillFocus, - accentFillHover, - accentFillRest, - bodyFont, - controlCornerRadius, - designUnit, - disabledOpacity, - focusStrokeWidth, - foregroundOnAccentActive, - foregroundOnAccentHover, - foregroundOnAccentRest, - neutralFillInputActive, - neutralFillInputHover, - neutralFillInputRest, - neutralForegroundRest, - neutralStrokeActive, - neutralStrokeHover, - neutralStrokeRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + fillColor, + focusStrokeOuter, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillInputActive, + neutralFillInputHover, + neutralFillInputRest, + neutralForegroundRest, + neutralStrokeActive, + neutralStrokeHover, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Checkbox * @public */ -export const checkboxStyles: FoundationElementTemplate< - ElementStyles, - CheckboxOptions -> = (context, definition) => - css` - ${display('inline-flex')} :host { - align-items: center; - outline: none; - margin: calc(${designUnit} * 1px) 0; - /* Chromium likes to select label text or the default slot when the checkbox is - clicked. Maybe there is a better solution here? */ - user-select: none; - } - - .control { - position: relative; - width: calc((${heightNumber} / 2 + ${designUnit}) * 1px); - height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); - box-sizing: border-box; - border-radius: calc(${controlCornerRadius} * 1px); - border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; - background: ${neutralFillInputRest}; - outline: none; - cursor: pointer; - } - - .label { - font-family: ${bodyFont}; - color: ${neutralForegroundRest}; - /* Need to discuss with Brian how HorizontalSpacingNumber can work. - https://github.com/microsoft/fast/issues/2766 */ - padding-inline-start: calc(${designUnit} * 2px + 2px); - margin-inline-end: calc(${designUnit} * 2px + 2px); - cursor: pointer; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - } - - .label__hidden { - display: none; - visibility: hidden; - } - - .checked-indicator { - width: 100%; - height: 100%; - display: block; - fill: ${foregroundOnAccentRest}; - opacity: 0; - pointer-events: none; - } - - .indeterminate-indicator { - border-radius: calc(${controlCornerRadius} * 1px); - background: ${foregroundOnAccentRest}; - position: absolute; - top: 50%; - left: 50%; - width: 50%; - height: 50%; - transform: translate(-50%, -50%); - opacity: 0; - } - - :host(:not([disabled])) .control:hover { - background: ${neutralFillInputHover}; - border-color: ${neutralStrokeHover}; - } - - :host(:not([disabled])) .control:active { - background: ${neutralFillInputActive}; - border-color: ${neutralStrokeActive}; - } - - :host(:${focusVisible}) .control { - outline: calc(${focusStrokeWidth} * 1px) solid ${accentFillFocus}; - outline-offset: 2px; - } - - :host([aria-checked='true']) .control { - background: ${accentFillRest}; - border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; - } - - :host([aria-checked='true']:not([disabled])) .control:hover { - background: ${accentFillHover}; - border: calc(${strokeWidth} * 1px) solid ${accentFillHover}; - } - - :host([aria-checked='true']:not([disabled])) - .control:hover - .checked-indicator { - fill: ${foregroundOnAccentHover}; - } - - :host([aria-checked='true']:not([disabled])) - .control:hover - .indeterminate-indicator { - background: ${foregroundOnAccentHover}; - } - - :host([aria-checked='true']:not([disabled])) .control:active { - background: ${accentFillActive}; - border: calc(${strokeWidth} * 1px) solid ${accentFillActive}; - } - - :host([aria-checked='true']:not([disabled])) - .control:active - .checked-indicator { - fill: ${foregroundOnAccentActive}; - } - - :host([aria-checked='true']:not([disabled])) - .control:active - .indeterminate-indicator { - background: ${foregroundOnAccentActive}; - } - - :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { - outline: calc(${focusStrokeWidth} * 1px) solid ${accentFillFocus}; - outline-offset: 2px; - } - - :host([disabled]) .label, - :host([readonly]) .label, - :host([readonly]) .control, - :host([disabled]) .control { - cursor: ${disabledCursor}; - } - - :host([aria-checked='true']:not(.indeterminate)) .checked-indicator, - :host(.indeterminate) .indeterminate-indicator { - opacity: 1; - } - - :host([disabled]) { - opacity: ${disabledOpacity}; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - .control { - forced-color-adjust: none; - border-color: ${SystemColors.FieldText}; - background: ${SystemColors.Field}; - } - .checked-indicator { - fill: ${SystemColors.FieldText}; - } - .indeterminate-indicator { - background: ${SystemColors.FieldText}; - } - :host(:not([disabled])) .control:hover, - .control:active { - border-color: ${SystemColors.Highlight}; - background: ${SystemColors.Field}; - } - :host(:${focusVisible}) .control { - outline: calc(${focusStrokeWidth} * 1px) solid ${SystemColors.FieldText}; - outline-offset: 2px; - } - :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { - outline: calc(${focusStrokeWidth} * 1px) solid ${SystemColors.FieldText}; - outline-offset: 2px; - } - :host([aria-checked='true']) .control { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.Highlight}; - } - :host([aria-checked='true']:not([disabled])) .control:hover, - .control:active { - border-color: ${SystemColors.Highlight}; - background: ${SystemColors.HighlightText}; - } - :host([aria-checked='true']) .checked-indicator { - fill: ${SystemColors.HighlightText}; - } - :host([aria-checked='true']:not([disabled])) - .control:hover +export const checkboxStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("inline-flex")} :host { + align-items: center; + outline: none; + margin: calc(${designUnit} * 1px) 0; + /* Chromium likes to select label text or the default slot when the checkbox is + clicked. Maybe there is a better solution here? */ + user-select: none; + } + + .control { + position: relative; + width: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + box-sizing: border-box; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; + background: ${neutralFillInputRest}; + outline: none; + cursor: pointer; + } + + .label { + font-family: ${bodyFont}; + color: ${neutralForegroundRest}; + padding-inline-start: calc(${designUnit} * 2px + 2px); + margin-inline-end: calc(${designUnit} * 2px + 2px); + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + } + + .label__hidden { + display: none; + visibility: hidden; + } + .checked-indicator { - fill: ${SystemColors.Highlight}; - } - :host([aria-checked='true']) .indeterminate-indicator { - background: ${SystemColors.HighlightText}; - } - :host([aria-checked='true']) .control:hover .indeterminate-indicator { - background: ${SystemColors.Highlight}; - } - :host([disabled]) { - opacity: 1; - } - :host([disabled]) .control { - forced-color-adjust: none; - border-color: ${SystemColors.GrayText}; - background: ${SystemColors.Field}; - } - :host([disabled]) .indeterminate-indicator, - :host([aria-checked='true'][disabled]) - .control:hover + width: 100%; + height: 100%; + display: block; + fill: ${foregroundOnAccentRest}; + opacity: 0; + pointer-events: none; + } + .indeterminate-indicator { - forced-color-adjust: none; - background: ${SystemColors.GrayText}; - } - :host([disabled]) .checked-indicator, - :host([aria-checked='true'][disabled]) .control:hover .checked-indicator { - forced-color-adjust: none; - fill: ${SystemColors.GrayText}; - } - `) - ); + border-radius: calc(${controlCornerRadius} * 1px); + background: ${foregroundOnAccentRest}; + position: absolute; + top: 50%; + left: 50%; + width: 50%; + height: 50%; + transform: translate(-50%, -50%); + opacity: 0; + } + + :host(:not([disabled])) .control:hover { + background: ${neutralFillInputHover}; + border-color: ${neutralStrokeHover}; + } + + :host(:not([disabled])) .control:active { + background: ${neutralFillInputActive}; + border-color: ${neutralStrokeActive}; + } + + :host(:${focusVisible}) .control { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } + + :host([aria-checked="true"]) .control { + background: ${accentFillRest}; + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + } + + :host([aria-checked="true"]:not([disabled])) .control:hover { + background: ${accentFillHover}; + border: calc(${strokeWidth} * 1px) solid ${accentFillHover}; + } + + :host([aria-checked="true"]:not([disabled])) .control:hover .checked-indicator { + fill: ${foregroundOnAccentHover}; + } + + :host([aria-checked="true"]:not([disabled])) .control:hover .indeterminate-indicator { + background: ${foregroundOnAccentHover}; + } + + :host([aria-checked="true"]:not([disabled])) .control:active { + background: ${accentFillActive}; + border: calc(${strokeWidth} * 1px) solid ${accentFillActive}; + } + + :host([aria-checked="true"]:not([disabled])) .control:active .checked-indicator { + fill: ${foregroundOnAccentActive}; + } + + :host([aria-checked="true"]:not([disabled])) .control:active .indeterminate-indicator { + background: ${foregroundOnAccentActive}; + } + + :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } + + + :host([disabled]) .label, + :host([readonly]) .label, + :host([readonly]) .control, + :host([disabled]) .control { + cursor: ${disabledCursor}; + } + + :host([aria-checked="true"]:not(.indeterminate)) .checked-indicator, + :host(.indeterminate) .indeterminate-indicator { + opacity: 1; + } + + :host([disabled]) { + opacity: ${disabledOpacity}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .control { + forced-color-adjust: none; + border-color: ${SystemColors.FieldText}; + background: ${SystemColors.Field}; + } + .checked-indicator { + fill: ${SystemColors.FieldText}; + } + .indeterminate-indicator { + background: ${SystemColors.FieldText}; + } + :host(:not([disabled])) .control:hover, .control:active { + border-color: ${SystemColors.Highlight}; + background: ${SystemColors.Field}; + } + :host(:${focusVisible}) .control { + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + :host([aria-checked="true"]) .control { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.Highlight}; + } + :host([aria-checked="true"]:not([disabled])) .control:hover, .control:active { + border-color: ${SystemColors.Highlight}; + background: ${SystemColors.HighlightText}; + } + :host([aria-checked="true"]) .checked-indicator { + fill: ${SystemColors.HighlightText}; + } + :host([aria-checked="true"]:not([disabled])) .control:hover .checked-indicator { + fill: ${SystemColors.Highlight} + } + :host([aria-checked="true"]) .indeterminate-indicator { + background: ${SystemColors.HighlightText}; + } + :host([aria-checked="true"]) .control:hover .indeterminate-indicator { + background: ${SystemColors.Highlight} + } + :host([disabled]) { + opacity: 1; + } + :host([disabled]) .control { + forced-color-adjust: none; + border-color: ${SystemColors.GrayText}; + background: ${SystemColors.Field}; + } + :host([disabled]) .indeterminate-indicator, + :host([aria-checked="true"][disabled]) .control:hover .indeterminate-indicator { + forced-color-adjust: none; + background: ${SystemColors.GrayText}; + } + :host([disabled]) .checked-indicator, + :host([aria-checked="true"][disabled]) .control:hover .checked-indicator { + forced-color-adjust: none; + fill: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/checkbox/index.ts b/packages/components/src/checkbox/index.ts index 33586393..b1d0f93e 100644 --- a/packages/components/src/checkbox/index.ts +++ b/packages/components/src/checkbox/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - Checkbox, - CheckboxOptions, - checkboxTemplate as template -} from '@microsoft/fast-foundation'; -import { checkboxStyles as styles } from './checkbox.styles'; + Checkbox, + CheckboxOptions, + checkboxTemplate as template, +} from "@microsoft/fast-foundation"; +import { checkboxStyles as styles } from "./checkbox.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Checkbox} registration for configuring the component with a DesignSystem. @@ -15,29 +12,29 @@ import { checkboxStyles as styles } from './checkbox.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpCheckbox = Checkbox.compose({ - baseName: 'checkbox', - template, - styles, - checkedIndicator: /* html */ ` - - - +export const fastCheckbox = Checkbox.compose({ + baseName: "checkbox", + template, + styles, + checkedIndicator: /* html */ ` + + + `, - indeterminateIndicator: /* html */ ` + indeterminateIndicator: /* html */ `
- ` + `, }); /** diff --git a/packages/components/src/color/README.md b/packages/components/src/color/README.md new file mode 100644 index 00000000..a0e4be23 --- /dev/null +++ b/packages/components/src/color/README.md @@ -0,0 +1,33 @@ +# FAST Color Recipes + +Color recipes are named colors who's value is algorithmically defined from a variety of inputs. `@microsoft/fast-components` relies on these recipes heavily to achieve expressive theming options while maintaining color accessability targets. + +## Swatch +A Swatch is a representation of a color that has a `relativeLuminance` value and a method to convert the swatch to a color string. It is used by recipes to determine which colors to use for UI. + +### SwatchRGB +A concrete implementation of `Swatch`, it is a swatch with red, green, and blue 64bit color channels . + +**Example: Creating a SwatchRGB** +```ts +import { SwatchRGB } from "@microsoft/fast-components"; + +const red = SwatchRGB.create(1, 0, 0); +``` + +## Palette +A palette is a collection `Swatch` instances, ordered by relative luminance, and provides mechanisms to safely retrieve swatches by index and by target contrast ratios. It also contains a `source` color, which is the color from which the palette is + +### PaletteRGB +An implementation of `Palette` of `SwatchRGB` instances. + +```ts +// Create a PaletteRGB from a SwatchRGB +const redPalette = PaletteRGB.from(red): + +// Create a PaletteRGB from an object +const greenPalette = PaletteRGB.from({r: 0, g: 1, b: 0}); + +// Create a PaletteRGB from R, G, and B arguments +const bluePalette = PaletteRGB.create(0, 0, 1); +``` \ No newline at end of file diff --git a/packages/components/src/color/palette.spec.ts b/packages/components/src/color/palette.spec.ts new file mode 100644 index 00000000..bd750f07 --- /dev/null +++ b/packages/components/src/color/palette.spec.ts @@ -0,0 +1,29 @@ + +import { expect } from "chai"; +import { PaletteRGB } from "./palette.js"; +import { SwatchRGB, isSwatchRGB } from "./swatch.js"; + +const test: SwatchRGB = { + r: 0, + g: 0, + b: 0, + relativeLuminance: 0, + contrast: () => 1, + toColorString: () => "" +} + +describe("PaletteRGB.from", () => { + it("should create a palette from the provided swatch if it matches a SwatchRGB implementation", () => { + const palette = PaletteRGB.from(test); + + expect(palette.source === test).to.be.true; + }) + + it("should create a palette from a rgb object", () => { + const source = {r: 1, g: 1, b: 1}; + const palette = PaletteRGB.from(source); + + expect(palette.source === source).to.be.false; + expect(isSwatchRGB(palette.source)).to.be.true; + }) +}); diff --git a/packages/components/src/color/palette.ts b/packages/components/src/color/palette.ts new file mode 100644 index 00000000..225d5edc --- /dev/null +++ b/packages/components/src/color/palette.ts @@ -0,0 +1,200 @@ +import { + clamp, + ColorRGBA64, + ComponentStateColorPalette, + parseColorHexRGB, +} from "@microsoft/fast-colors"; +import { isSwatchRGB, Swatch, SwatchRGB } from "./swatch.js"; +import { binarySearch } from "./utilities/binary-search.js"; +import { directionByIsDark } from "./utilities/direction-by-is-dark.js"; +import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js"; + +/** + * A collection of {@link Swatch} instances + * @public + */ +export interface Palette { + readonly source: T; + readonly swatches: ReadonlyArray; + + /** + * Returns a swatch from the palette that most closely matches + * the contrast ratio provided to a provided reference. + */ + colorContrast( + reference: Swatch, + contrast: number, + initialIndex?: number, + direction?: 1 | -1 + ): T; + + /** + * Returns the index of the palette that most closely matches + * the relativeLuminance of the provided swatch + */ + closestIndexOf(reference: RelativeLuminance): number; + + /** + * Gets a swatch by index. Index is clamped to the limits + * of the palette so a Swatch will always be returned. + */ + get(index: number): T; +} + +/** @public */ +export type PaletteRGB = Palette; + +/** + * Creates a PaletteRGB from input R, G, B color values. + * @param r - Red value represented as a number between 0 and 1. + * @param g - Green value represented as a number between 0 and 1. + * @param b - Blue value represented as a number between 0 and 1. + */ +function create(r: number, g: number, b: number): PaletteRGB; +/** + * Creates a PaletteRGB from a source SwatchRGB object. + * @deprecated - Use PaletteRGB.from() + */ +function create(source: SwatchRGB): PaletteRGB; +function create(rOrSource: SwatchRGB | number, g?: number, b?: number): PaletteRGB { + if (typeof rOrSource === "number") { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return PaletteRGB.from(SwatchRGB.create(rOrSource, g!, b!)); + } else { + return PaletteRGB.from(rOrSource); + } +} + +/** + * Creates a PaletteRGB from a source color object. + * @param source - The source color + */ +function from(source: SwatchRGB): PaletteRGB; +function from(source: Record<"r" | "g" | "b", number>): PaletteRGB; +function from(source: any): PaletteRGB { + return isSwatchRGB(source) + ? PaletteRGBImpl.from(source) + : PaletteRGBImpl.from(SwatchRGB.create(source.r, source.g, source.b)); +} +/** @public */ +export const PaletteRGB = Object.freeze({ + create, + from, +}); + +/** + * A {@link Palette} representing RGB swatch values. + * @public + */ +class PaletteRGBImpl implements Palette { + /** + * {@inheritdoc Palette.source} + */ + public readonly source: SwatchRGB; + public readonly swatches: ReadonlyArray; + private lastIndex: number; + private reversedSwatches: ReadonlyArray; + private closestIndexCache = new Map(); + + /** + * + * @param source - The source color for the palette + * @param swatches - All swatches in the palette + */ + constructor(source: SwatchRGB, swatches: ReadonlyArray) { + this.source = source; + this.swatches = swatches; + + this.reversedSwatches = Object.freeze([...this.swatches].reverse()); + this.lastIndex = this.swatches.length - 1; + } + + /** + * {@inheritdoc Palette.colorContrast} + */ + public colorContrast( + reference: Swatch, + contrastTarget: number, + initialSearchIndex?: number, + direction?: 1 | -1 + ): SwatchRGB { + if (initialSearchIndex === undefined) { + initialSearchIndex = this.closestIndexOf(reference); + } + + let source: ReadonlyArray = this.swatches; + const endSearchIndex = this.lastIndex; + let startSearchIndex = initialSearchIndex; + + if (direction === undefined) { + direction = directionByIsDark(reference); + } + + const condition = (value: SwatchRGB) => + contrast(reference, value) >= contrastTarget; + + if (direction === -1) { + source = this.reversedSwatches; + startSearchIndex = endSearchIndex - startSearchIndex; + } + + return binarySearch(source, condition, startSearchIndex, endSearchIndex); + } + + /** + * {@inheritdoc Palette.get} + */ + public get(index: number): SwatchRGB { + return this.swatches[index] || this.swatches[clamp(index, 0, this.lastIndex)]; + } + + /** + * {@inheritdoc Palette.closestIndexOf} + */ + public closestIndexOf(reference: Swatch): number { + if (this.closestIndexCache.has(reference.relativeLuminance)) { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + return this.closestIndexCache.get(reference.relativeLuminance)!; + } + + let index = this.swatches.indexOf(reference as SwatchRGB); + + if (index !== -1) { + this.closestIndexCache.set(reference.relativeLuminance, index); + return index; + } + + const closest = this.swatches.reduce((previous, next) => + Math.abs(next.relativeLuminance - reference.relativeLuminance) < + Math.abs(previous.relativeLuminance - reference.relativeLuminance) + ? next + : previous + ); + + index = this.swatches.indexOf(closest); + this.closestIndexCache.set(reference.relativeLuminance, index); + + return index; + } + + /** + * Create a color palette from a provided swatch + * @param source - The source swatch to create a palette from + * @returns + */ + static from(source: SwatchRGB): PaletteRGB { + return new PaletteRGBImpl( + source, + Object.freeze( + new ComponentStateColorPalette({ + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + baseColor: ColorRGBA64.fromObject(source)!, + }).palette.map(x => { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const _x = parseColorHexRGB(x.toStringHexRGB())!; + return SwatchRGB.create(_x.r, _x.g, _x.b); + }) + ) + ); + } +} diff --git a/packages/components/src/color/recipe.ts b/packages/components/src/color/recipe.ts new file mode 100644 index 00000000..0bff1a60 --- /dev/null +++ b/packages/components/src/color/recipe.ts @@ -0,0 +1,24 @@ +import { Swatch } from "./swatch.js"; + +/** @public */ +export interface InteractiveSwatchSet { + /** + * The swatch to apply to the rest state + */ + rest: Swatch; + + /** + * The swatch to apply to the hover state + */ + hover: Swatch; + + /** + * The swatch to apply to the active state + */ + active: Swatch; + + /** + * The swatch to apply to the focus state + */ + focus: Swatch; +} diff --git a/packages/components/src/color/recipes/accent-fill.ts b/packages/components/src/color/recipes/accent-fill.ts new file mode 100644 index 00000000..f0bf8d62 --- /dev/null +++ b/packages/components/src/color/recipes/accent-fill.ts @@ -0,0 +1,40 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; + +/** + * @internal + */ +export function accentFill( + palette: Palette, + neutralPalette: Palette, + reference: Swatch, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + neutralFillRestDelta: number, + neutralFillHoverDelta: number, + neutralFillActiveDelta: number +): InteractiveSwatchSet { + const accent = palette.source; + const referenceIndex = neutralPalette.closestIndexOf(reference); + const swapThreshold = Math.max( + neutralFillRestDelta, + neutralFillHoverDelta, + neutralFillActiveDelta + ); + const direction = referenceIndex >= swapThreshold ? -1 : 1; + const accentIndex = palette.closestIndexOf(accent); + + const hoverIndex = accentIndex; + const restIndex = hoverIndex + direction * -1 * hoverDelta; + const activeIndex = restIndex + direction * activeDelta; + const focusIndex = restIndex + direction * focusDelta; + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(activeIndex), + focus: palette.get(focusIndex), + }; +} diff --git a/packages/components/src/color/recipes/accent-foreground.spec.ts b/packages/components/src/color/recipes/accent-foreground.spec.ts new file mode 100644 index 00000000..7bde416a --- /dev/null +++ b/packages/components/src/color/recipes/accent-foreground.spec.ts @@ -0,0 +1,95 @@ +import { expect } from "chai"; +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { PaletteRGB } from "../palette.js"; +import { SwatchRGB } from "../swatch.js"; +import { accentBase, black, middleGrey, white } from "../utilities/color-constants.js"; +import { accentForeground } from "./accent-foreground.js"; + +describe("accentForeground", (): void => { + const neutralPalette = PaletteRGB.create(middleGrey) + const accentPalette = PaletteRGB.create(accentBase); + + it("should increase contrast on hover state and decrease contrast on active state in either mode", (): void => { + const lightModeColors = accentForeground( + accentPalette, + white, + 4.5, + 0, + 6, + -4, + 0 + ); + const darkModeColors = accentForeground( + accentPalette, + black, + 4.5, + 0, + 6, + -4, + 0 + ); + + expect( + lightModeColors.hover.contrast(white) + ).to.be.greaterThan( + lightModeColors.rest.contrast(white) + ); + expect( + darkModeColors.hover.contrast(black) + ).to.be.greaterThan( + darkModeColors.rest.contrast(black) + ); + }); + + it("should have accessible rest and hover colors against the background color", (): void => { + const accentColors = [ + SwatchRGB.from(parseColorHexRGB("#0078D4")!), + SwatchRGB.from(parseColorHexRGB("#107C10")!), + SwatchRGB.from(parseColorHexRGB("#5C2D91")!), + SwatchRGB.from(parseColorHexRGB("#D83B01")!), + SwatchRGB.from(parseColorHexRGB("#F2C812")!), + ]; + + accentColors.forEach( + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + (accent): void => { + const accentPalette = PaletteRGB.create(accent); + + neutralPalette.swatches.forEach((swatch): void => { + const smallColors = accentForeground( + accentPalette, + swatch, + 4.5, + 0, + 6, + -4, + 0 + ); + const largeColors = accentForeground( + accentPalette, + swatch, + 3, + 0, + 6, + -4, + 0 + ); + expect( + swatch.contrast(smallColors.rest) + // There are a few states that are impossible to meet contrast on + ).to.be.gte(4.47); + expect( + swatch.contrast(smallColors.hover) + // There are a few states that are impossible to meet contrast on + ).to.be.gte(3.7); + expect( + swatch.contrast(largeColors.rest) + ).to.be.gte(3); + expect( + swatch.contrast(largeColors.hover) + ).to.be.gte(3); + }); + } + ); + }); +}); diff --git a/packages/components/src/color/recipes/accent-foreground.ts b/packages/components/src/color/recipes/accent-foreground.ts new file mode 100644 index 00000000..1c2fb47c --- /dev/null +++ b/packages/components/src/color/recipes/accent-foreground.ts @@ -0,0 +1,57 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * @internal + */ +export function accentForeground( + palette: Palette, + reference: Swatch, + contrastTarget: number, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +): InteractiveSwatchSet { + const accent = palette.source; + const accentIndex = palette.closestIndexOf(accent); + const direction = directionByIsDark(reference); + const startIndex = + accentIndex + + (direction === 1 + ? Math.min(restDelta, hoverDelta) + : Math.max(direction * restDelta, direction * hoverDelta)); + const accessibleSwatch = palette.colorContrast( + reference, + contrastTarget, + startIndex, + direction + ); + const accessibleIndex1 = palette.closestIndexOf(accessibleSwatch); + const accessibleIndex2 = + accessibleIndex1 + direction * Math.abs(restDelta - hoverDelta); + const indexOneIsRestState = + direction === 1 + ? restDelta < hoverDelta + : direction * restDelta > direction * hoverDelta; + + let restIndex: number; + let hoverIndex: number; + + if (indexOneIsRestState) { + restIndex = accessibleIndex1; + hoverIndex = accessibleIndex2; + } else { + restIndex = accessibleIndex2; + hoverIndex = accessibleIndex1; + } + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(restIndex + direction * activeDelta), + focus: palette.get(restIndex + direction * focusDelta), + }; +} diff --git a/packages/components/src/color/recipes/focus-stroke.ts b/packages/components/src/color/recipes/focus-stroke.ts new file mode 100644 index 00000000..f3e79f5c --- /dev/null +++ b/packages/components/src/color/recipes/focus-stroke.ts @@ -0,0 +1,22 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** @internal */ +export function focusStrokeOuter(palette: Palette, reference: Swatch) { + return palette.colorContrast(reference, 3.5); +} + +/** @internal */ +export function focusStrokeInner( + palette: Palette, + reference: Swatch, + focusColor: Swatch +): Swatch { + return palette.colorContrast( + focusColor, + 3.5, + palette.closestIndexOf(palette.source), + (directionByIsDark(reference) * -1) as 1 | -1 + ); +} diff --git a/packages/components/src/color/recipes/foreground-on-accent.spec.ts b/packages/components/src/color/recipes/foreground-on-accent.spec.ts new file mode 100644 index 00000000..91a5a8a4 --- /dev/null +++ b/packages/components/src/color/recipes/foreground-on-accent.spec.ts @@ -0,0 +1,19 @@ +import { expect } from "chai"; +import { SwatchRGB } from "../swatch.js"; +import { black } from "../utilities/color-constants.js"; +import { foregroundOnAccent } from './foreground-on-accent'; + +describe("Cut text", (): void => { + it("should return black when background does not meet contrast ratio", (): void => { + const small = foregroundOnAccent(SwatchRGB.create(1, 1, 1), 4.5) as SwatchRGB; + const large = foregroundOnAccent(SwatchRGB.create(1, 1, 1), 3) as SwatchRGB; + + expect(small.r).to.equal(black.r); + expect(small.g).to.equal(black.g); + expect(small.b).to.equal(black.b); + + expect(large.r).to.equal(black.r); + expect(large.g).to.equal(black.g); + expect(large.b).to.equal(black.b); + }); +}); diff --git a/packages/components/src/color/recipes/foreground-on-accent.ts b/packages/components/src/color/recipes/foreground-on-accent.ts new file mode 100644 index 00000000..79f4b029 --- /dev/null +++ b/packages/components/src/color/recipes/foreground-on-accent.ts @@ -0,0 +1,9 @@ +import { Swatch } from "../swatch.js"; +import { black, white } from "../utilities/color-constants.js"; + +/** + * @internal + */ +export function foregroundOnAccent(reference: Swatch, contrastTarget: number): Swatch { + return reference.contrast(white) >= contrastTarget ? white : black; +} diff --git a/packages/components/src/color/recipes/neutral-fill-contrast.ts b/packages/components/src/color/recipes/neutral-fill-contrast.ts new file mode 100644 index 00000000..0027cafc --- /dev/null +++ b/packages/components/src/color/recipes/neutral-fill-contrast.ts @@ -0,0 +1,42 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * @internal + */ +export function neutralFillContrast( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +): InteractiveSwatchSet { + const direction = directionByIsDark(reference); + const accessibleIndex = palette.closestIndexOf(palette.colorContrast(reference, 4.5)); + const accessibleIndex2 = + accessibleIndex + direction * Math.abs(restDelta - hoverDelta); + const indexOneIsRest = + direction === 1 + ? restDelta < hoverDelta + : direction * restDelta > direction * hoverDelta; + let restIndex: number; + let hoverIndex: number; + + if (indexOneIsRest) { + restIndex = accessibleIndex; + hoverIndex = accessibleIndex2; + } else { + restIndex = accessibleIndex2; + hoverIndex = accessibleIndex; + } + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(restIndex + direction * activeDelta), + focus: palette.get(restIndex + direction * focusDelta), + }; +} diff --git a/packages/components/src/color/recipes/neutral-fill-input.ts b/packages/components/src/color/recipes/neutral-fill-input.ts new file mode 100644 index 00000000..6f4272ec --- /dev/null +++ b/packages/components/src/color/recipes/neutral-fill-input.ts @@ -0,0 +1,26 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * @internal + */ +export function neutralFillInput( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +): InteractiveSwatchSet { + const direction = directionByIsDark(reference); + const referenceIndex = palette.closestIndexOf(reference); + + return { + rest: palette.get(referenceIndex - direction * restDelta), + hover: palette.get(referenceIndex - direction * hoverDelta), + active: palette.get(referenceIndex - direction * activeDelta), + focus: palette.get(referenceIndex - direction * focusDelta), + }; +} diff --git a/packages/components/src/color/recipes/neutral-fill-layer.spec.ts b/packages/components/src/color/recipes/neutral-fill-layer.spec.ts new file mode 100644 index 00000000..a166291e --- /dev/null +++ b/packages/components/src/color/recipes/neutral-fill-layer.spec.ts @@ -0,0 +1,31 @@ +import { expect } from "chai"; +import { PaletteRGB } from "../palette.js"; +import { SwatchRGB } from "../swatch.js"; +import { middleGrey } from "../utilities/color-constants.js"; +import { neutralFillLayer } from "./neutral-fill-layer.js"; + +const neutralPalette = PaletteRGB.create(middleGrey); + +describe("neutralFillCard", (): void => { + it("should get darker when the index of the backgroundColor is lower than the offset index", (): void => { + const delta = 3 + for (let i: number = 0; i < delta; i++) { + const color = neutralFillLayer(neutralPalette, neutralPalette.get(i), delta) + const resolved = neutralPalette.get(delta + i); + expect( + color + ).to.equal(resolved); + } + }); + it("should return the color at three steps lower than the background color", (): void => { + const delta = 3; + + for (let i: number = delta; i < neutralPalette.swatches.length; i++) { + expect( + neutralPalette.swatches.indexOf( + neutralFillLayer(neutralPalette, neutralPalette.get(i), delta) as SwatchRGB + ) + ).to.equal(i - 3); + } + }); +}); diff --git a/packages/components/src/color/recipes/neutral-fill-layer.ts b/packages/components/src/color/recipes/neutral-fill-layer.ts new file mode 100644 index 00000000..67f6ffac --- /dev/null +++ b/packages/components/src/color/recipes/neutral-fill-layer.ts @@ -0,0 +1,15 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; + +/** + * @internal + */ +export function neutralFillLayer( + palette: Palette, + reference: Swatch, + delta: number +): Swatch { + const referenceIndex = palette.closestIndexOf(reference); + + return palette.get(referenceIndex - (referenceIndex < delta ? delta * -1 : delta)); +} diff --git a/packages/components/src/color/recipes/neutral-fill-stealth.ts b/packages/components/src/color/recipes/neutral-fill-stealth.ts new file mode 100644 index 00000000..79019779 --- /dev/null +++ b/packages/components/src/color/recipes/neutral-fill-stealth.ts @@ -0,0 +1,40 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; + +/** + * @internal + */ +export function neutralFillStealth( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number, + fillRestDelta: number, + fillHoverDelta: number, + fillActiveDelta: number, + fillFocusDelta: number +): InteractiveSwatchSet { + const swapThreshold = Math.max( + restDelta, + hoverDelta, + activeDelta, + focusDelta, + fillRestDelta, + fillHoverDelta, + fillActiveDelta, + fillFocusDelta + ); + + const referenceIndex = palette.closestIndexOf(reference); + const direction: 1 | -1 = referenceIndex >= swapThreshold ? -1 : 1; + + return { + rest: palette.get(referenceIndex + direction * restDelta), + hover: palette.get(referenceIndex + direction * hoverDelta), + active: palette.get(referenceIndex + direction * activeDelta), + focus: palette.get(referenceIndex + direction * focusDelta), + }; +} diff --git a/packages/components/src/color/recipes/neutral-fill.ts b/packages/components/src/color/recipes/neutral-fill.ts new file mode 100644 index 00000000..00f6bdbb --- /dev/null +++ b/packages/components/src/color/recipes/neutral-fill.ts @@ -0,0 +1,33 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; + +/** + * + * @param palette - The palette to operate on + * @param reference - The reference color to calculate a color for + * @param delta - The offset from the reference's location + * @param threshold - Determines if a lighter or darker color than the reference will be picked. + * @returns + * + * @internal + */ +export function neutralFill( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +): InteractiveSwatchSet { + const referenceIndex = palette.closestIndexOf(reference); + const threshold = Math.max(restDelta, hoverDelta, activeDelta, focusDelta); + const direction = referenceIndex >= threshold ? -1 : 1; + + return { + rest: palette.get(referenceIndex + direction * restDelta), + hover: palette.get(referenceIndex + direction * hoverDelta), + active: palette.get(referenceIndex + direction * activeDelta), + focus: palette.get(referenceIndex + direction * focusDelta), + }; +} diff --git a/packages/components/src/color/recipes/neutral-foreground-hint.spec.ts b/packages/components/src/color/recipes/neutral-foreground-hint.spec.ts new file mode 100644 index 00000000..5b4d625a --- /dev/null +++ b/packages/components/src/color/recipes/neutral-foreground-hint.spec.ts @@ -0,0 +1,35 @@ +import { expect } from "chai"; +import { PaletteRGB } from "../palette.js"; +import { SwatchRGB } from "../swatch.js"; +import { accentBase, middleGrey } from "../utilities/color-constants.js"; +import { neutralForegroundHint } from "./neutral-foreground-hint.js"; + +describe("neutralForegroundHint", (): void => { + const neutralPalette = PaletteRGB.create(middleGrey); + const accentPalette = PaletteRGB.create(accentBase); + + neutralPalette.swatches.concat(accentPalette.swatches).forEach((swatch): void => { + it(`${swatch} should resolve a color from the neutral palette`, (): void => { + expect( + neutralPalette.swatches.indexOf( + neutralForegroundHint( + neutralPalette, + swatch + ) as SwatchRGB + ) + ).not.to.equal(-1); + }); + }); + + neutralPalette.swatches.concat(accentPalette.swatches).forEach((swatch): void => { + it(`${swatch} should always be at least 4.5 : 1 against the background`, (): void => { + expect( + swatch.contrast(neutralForegroundHint(neutralPalette, swatch)) + // retrieveContrast(swatch, neutralForegroundHint_DEPRECATED) + // Because neutralForegroundHint follows the direction patterns of neutralForeground, + // a backgroundColor #777777 is impossible to hit 4.5 against. + ).to.be.gte(swatch.toColorString().toUpperCase() === "#777777" ? 4.48 : 4.5); + expect(swatch.contrast(neutralForegroundHint(neutralPalette, swatch))).to.be.lessThan(5); + }); + }); +}); diff --git a/packages/components/src/color/recipes/neutral-foreground-hint.ts b/packages/components/src/color/recipes/neutral-foreground-hint.ts new file mode 100644 index 00000000..e11696cd --- /dev/null +++ b/packages/components/src/color/recipes/neutral-foreground-hint.ts @@ -0,0 +1,13 @@ +import { Swatch } from "../swatch.js"; +import { Palette } from "../palette.js"; + +/** + * The neutralForegroundHint color recipe + * @param palette - The palette to operate on + * @param reference - The reference color + * + * @internal + */ +export function neutralForegroundHint(palette: Palette, reference: Swatch): Swatch { + return palette.colorContrast(reference, 4.5); +} diff --git a/packages/components/src/color/recipes/neutral-foreground.spec.ts b/packages/components/src/color/recipes/neutral-foreground.spec.ts new file mode 100644 index 00000000..aa71745e --- /dev/null +++ b/packages/components/src/color/recipes/neutral-foreground.spec.ts @@ -0,0 +1,22 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { expect } from "chai"; +import { PaletteRGB } from "../palette.js"; +import { neutralForeground } from "./neutral-foreground.js"; +import { SwatchRGB } from "../swatch.js"; +import { middleGrey, white } from "../utilities/color-constants.js"; + +describe("neutralForeground", (): void => { + const neutralPalette = PaletteRGB.create(middleGrey); + + it("should return correct result with default design system values", (): void => { + expect( + neutralForeground(neutralPalette, neutralPalette.get(88)).contrast(neutralPalette.get(neutralPalette.swatches.length - 1)) + ).to.be.gte(14); + }); + + it("should return #FFFFFF with a dark background", (): void => { + expect( + neutralForeground(neutralPalette, white).contrast(white) + ).to.be.gte(14); + }); +}); diff --git a/packages/components/src/color/recipes/neutral-foreground.ts b/packages/components/src/color/recipes/neutral-foreground.ts new file mode 100644 index 00000000..5d1707d1 --- /dev/null +++ b/packages/components/src/color/recipes/neutral-foreground.ts @@ -0,0 +1,9 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; + +/** + * @internal + */ +export function neutralForeground(palette: Palette, reference: Swatch): Swatch { + return palette.colorContrast(reference, 14); +} diff --git a/packages/components/src/color/recipes/neutral-layer-1.ts b/packages/components/src/color/recipes/neutral-layer-1.ts new file mode 100644 index 00000000..b9073fdb --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer-1.ts @@ -0,0 +1,9 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { baseLayerLuminanceSwatch } from "../utilities/base-layer-luminance.js"; + +export function neutralLayer1(palette: Palette, baseLayerLuminance: number): Swatch { + return palette.get( + palette.closestIndexOf(baseLayerLuminanceSwatch(baseLayerLuminance)) + ); +} diff --git a/packages/components/src/color/recipes/neutral-layer-2.ts b/packages/components/src/color/recipes/neutral-layer-2.ts new file mode 100644 index 00000000..aeaf440f --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer-2.ts @@ -0,0 +1,45 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { baseLayerLuminanceSwatch } from "../utilities/base-layer-luminance.js"; + +/** + * @internal + */ +export function neutralLayer2Index( + palette: Palette, + luminance: number, + layerDelta: number, + fillRestDelta: number, + fillHoverDelta: number, + fillActiveDelta: number +): number { + return Math.max( + palette.closestIndexOf(baseLayerLuminanceSwatch(luminance)) + layerDelta, + fillRestDelta, + fillHoverDelta, + fillActiveDelta + ); +} + +/** + * @internal + */ +export function neutralLayer2( + palette: Palette, + luminance: number, + layerDelta: number, + fillRestDelta: number, + fillHoverDelta: number, + fillActiveDelta: number +): Swatch { + return palette.get( + neutralLayer2Index( + palette, + luminance, + layerDelta, + fillRestDelta, + fillHoverDelta, + fillActiveDelta + ) + ); +} diff --git a/packages/components/src/color/recipes/neutral-layer-3.ts b/packages/components/src/color/recipes/neutral-layer-3.ts new file mode 100644 index 00000000..9075dcc2 --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer-3.ts @@ -0,0 +1,26 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { neutralLayer2Index } from "./neutral-layer-2.js"; + +/** + * @internal + */ +export function neutralLayer3( + palette: Palette, + luminance: number, + layerDelta: number, + fillRestDelta: number, + fillHoverDelta: number, + fillActiveDelta: number +): Swatch { + return palette.get( + neutralLayer2Index( + palette, + luminance, + layerDelta, + fillRestDelta, + fillHoverDelta, + fillActiveDelta + ) + layerDelta + ); +} diff --git a/packages/components/src/color/recipes/neutral-layer-4.ts b/packages/components/src/color/recipes/neutral-layer-4.ts new file mode 100644 index 00000000..2656bdc0 --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer-4.ts @@ -0,0 +1,27 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { neutralLayer2Index } from "./neutral-layer-2.js"; + +/** + * @internal + */ +export function neutralLayer4( + palette: Palette, + luminance: number, + layerDelta: number, + fillRestDelta: number, + fillHoverDelta: number, + fillActiveDelta: number +): Swatch { + return palette.get( + neutralLayer2Index( + palette, + luminance, + layerDelta, + fillRestDelta, + fillHoverDelta, + fillActiveDelta + ) + + layerDelta * 2 + ); +} diff --git a/packages/components/src/color/recipes/neutral-layer-card-container.ts b/packages/components/src/color/recipes/neutral-layer-card-container.ts new file mode 100644 index 00000000..1fd1418f --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer-card-container.ts @@ -0,0 +1,16 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { baseLayerLuminanceSwatch } from "../utilities/base-layer-luminance.js"; + +/** + * @internal + */ +export function neutralLayerCardContainer( + palette: Palette, + relativeLuminance: number, + layerDelta: number +): Swatch { + return palette.get( + palette.closestIndexOf(baseLayerLuminanceSwatch(relativeLuminance)) + layerDelta + ); +} diff --git a/packages/components/src/color/recipes/neutral-layer-floating.ts b/packages/components/src/color/recipes/neutral-layer-floating.ts new file mode 100644 index 00000000..e84d5039 --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer-floating.ts @@ -0,0 +1,16 @@ +import { Palette } from "../palette.js"; +import { Swatch } from "../swatch.js"; +import { baseLayerLuminanceSwatch } from "../utilities/base-layer-luminance.js"; + +/** + * @internal + */ +export function neutralLayerFloating( + palette: Palette, + relativeLuminance: number, + layerDelta: number +): Swatch { + const cardIndex = + palette.closestIndexOf(baseLayerLuminanceSwatch(relativeLuminance)) - layerDelta; + return palette.get(cardIndex - layerDelta); +} diff --git a/packages/components/src/color/recipes/neutral-layer.spec.ts b/packages/components/src/color/recipes/neutral-layer.spec.ts new file mode 100644 index 00000000..6628716c --- /dev/null +++ b/packages/components/src/color/recipes/neutral-layer.spec.ts @@ -0,0 +1,72 @@ +import { expect } from "chai"; +import { PaletteRGB } from "../palette.js"; +import { StandardLuminance } from "../utilities/base-layer-luminance.js"; +import { middleGrey } from "../utilities/color-constants.js"; +import { + neutralLayerFloating +} from './neutral-layer-floating'; +import { neutralLayer1 } from "./neutral-layer-1.js"; +import { neutralLayer2 } from "./neutral-layer-2.js"; +import { neutralLayer3 } from "./neutral-layer-3.js"; +import { neutralLayer4 } from "./neutral-layer-4.js"; +import { SwatchRGB } from "../swatch.js"; + +const neutralPalette = PaletteRGB.create(middleGrey); + +const enum NeutralPaletteLightModeOffsets { + L1 = 0, + L2 = 10, + L3 = 13, + L4 = 16, +} + +const enum NeutralPaletteDarkModeOffsets { + L1 = 76, + L2 = 79, + L3 = 82, + L4 = 85, +} + +describe("neutralLayer", (): void => { + describe("1", (): void => { + it("should return values from 1 when in light mode", (): void => { + expect(neutralLayer1(neutralPalette, StandardLuminance.LightMode)).to.equal(neutralPalette.get(NeutralPaletteLightModeOffsets.L1)) + }); + it("should return values from 1 when in dark mode", (): void => { + expect(neutralLayer1(neutralPalette, StandardLuminance.DarkMode)).to.equal(neutralPalette.get(NeutralPaletteDarkModeOffsets.L1)) + }); + }); + + describe("2", (): void => { + it("should return values from 2 when in light mode", (): void => { + expect(neutralLayer2(neutralPalette, StandardLuminance.LightMode, 3, 7, 10, 5)).to.equal(neutralPalette.get(NeutralPaletteLightModeOffsets.L2)) + }); + it("should return values from 2 when in dark mode", (): void => { + expect(neutralLayer2(neutralPalette, StandardLuminance.DarkMode, 3, 7, 10, 5)).to.equal(neutralPalette.get(NeutralPaletteDarkModeOffsets.L2)) + }); + }); + + describe("3", (): void => { + it("should return values from 3 when in light mode", (): void => { + expect(neutralLayer3(neutralPalette, StandardLuminance.LightMode, 3, 7, 10, 5)).to.equal(neutralPalette.get(NeutralPaletteLightModeOffsets.L3)) + }); + it("should return values from 3 when in dark mode", (): void => { + expect(neutralLayer3(neutralPalette, StandardLuminance.DarkMode, 3, 7, 10, 5)).to.equal(neutralPalette.get(NeutralPaletteDarkModeOffsets.L3)) + }); + }); + + describe("4", (): void => { + it("should return values from 4 when in light mode", (): void => { + expect(neutralLayer4(neutralPalette, StandardLuminance.LightMode, 3, 7, 10, 5)).to.equal(neutralPalette.get(NeutralPaletteLightModeOffsets.L4)) + }); + it("should return values from 4 when in dark mode", (): void => { + expect(neutralLayer4(neutralPalette, StandardLuminance.DarkMode, 3, 7, 10, 5)).to.equal(neutralPalette.get(NeutralPaletteDarkModeOffsets.L4)) + }); + }); + + describe("neutralLayerFloating", (): void => { + it("should return a color from the neutral palette", (): void => { + expect(neutralPalette.swatches.includes(neutralLayerFloating(neutralPalette, StandardLuminance.LightMode, 3) as SwatchRGB)).to.be.true; + }); + }); +}); diff --git a/packages/components/src/color/recipes/neutral-stroke-divider.ts b/packages/components/src/color/recipes/neutral-stroke-divider.ts new file mode 100644 index 00000000..d011ff3e --- /dev/null +++ b/packages/components/src/color/recipes/neutral-stroke-divider.ts @@ -0,0 +1,21 @@ +import { Swatch } from "../swatch.js"; +import { Palette } from "../palette.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * The neutralStrokeDivider color recipe + * @param palette - The palette to operate on + * @param reference - The reference color + * @param delta - The offset from the reference + * + * @internal + */ +export function neutralStrokeDivider( + palette: Palette, + reference: Swatch, + delta: number +): Swatch { + return palette.get( + palette.closestIndexOf(reference) + directionByIsDark(reference) * delta + ); +} diff --git a/packages/components/src/color/recipes/neutral-stroke.ts b/packages/components/src/color/recipes/neutral-stroke.ts new file mode 100644 index 00000000..1107dee2 --- /dev/null +++ b/packages/components/src/color/recipes/neutral-stroke.ts @@ -0,0 +1,31 @@ +import { Palette } from "../palette.js"; +import { InteractiveSwatchSet } from "../recipe.js"; +import { Swatch } from "../swatch.js"; +import { directionByIsDark } from "../utilities/direction-by-is-dark.js"; + +/** + * @internal + */ +export function neutralStroke( + palette: Palette, + reference: Swatch, + restDelta: number, + hoverDelta: number, + activeDelta: number, + focusDelta: number +): InteractiveSwatchSet { + const referenceIndex = palette.closestIndexOf(reference); + const direction = directionByIsDark(reference); + + const restIndex = referenceIndex + direction * restDelta; + const hoverIndex = restIndex + direction * (hoverDelta - restDelta); + const activeIndex = restIndex + direction * (activeDelta - restDelta); + const focusIndex = restIndex + direction * (focusDelta - restDelta); + + return { + rest: palette.get(restIndex), + hover: palette.get(hoverIndex), + active: palette.get(activeIndex), + focus: palette.get(focusIndex), + }; +} diff --git a/packages/components/src/color/swatch.spec.ts b/packages/components/src/color/swatch.spec.ts new file mode 100644 index 00000000..5e7a763e --- /dev/null +++ b/packages/components/src/color/swatch.spec.ts @@ -0,0 +1,38 @@ + +import { expect } from "chai"; +import { SwatchRGB, isSwatchRGB } from "./swatch.js"; + +const test: SwatchRGB = { + r: 0, + g: 0, + b: 0, + relativeLuminance: 0, + contrast: () => 1, + toColorString: () => "" +} + +describe("isSwatchRGB", () => { + it("should return true when called with the product of SwatchRGB.create()", () => { + expect(isSwatchRGB(SwatchRGB.create(1, 1, 1))).to.be.true; + }); + + it("should return true when called with an object conforming to the interface", () => { + expect(isSwatchRGB(test)).to.be.true; + }) + + for (const key in test ) { + it(`should return false when called with an object missing the ${key} property`, () => { + const _test = {...test}; + delete _test[key]; + + expect(isSwatchRGB(_test)).to.be.false; + }); + + it(`should return false when called with an object with the ${key} property assigned to a mismatching type`, () => { + const _test = {...test}; + _test[key] = "foobar"; + + expect(isSwatchRGB(_test)).to.be.false; + }) + } +}); diff --git a/packages/components/src/color/swatch.ts b/packages/components/src/color/swatch.ts new file mode 100644 index 00000000..068f9975 --- /dev/null +++ b/packages/components/src/color/swatch.ts @@ -0,0 +1,77 @@ +import { ColorRGBA64, rgbToRelativeLuminance } from "@microsoft/fast-colors"; +import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js"; + +/** + * Represents a color in a {@link Palette} + * @public + */ +export interface Swatch extends RelativeLuminance { + toColorString(): string; + contrast(target: RelativeLuminance): number; +} + +/** @public */ +export interface SwatchRGB extends Swatch { + r: number; + g: number; + b: number; +} + +/** @public */ +export const SwatchRGB = Object.freeze({ + create(r: number, g: number, b: number): SwatchRGB { + return new SwatchRGBImpl(r, g, b); + }, + from(obj: { r: number; g: number; b: number }): SwatchRGB { + return new SwatchRGBImpl(obj.r, obj.g, obj.b); + }, +}); + +/** + * Runtime test for an objects conformance with the SwatchRGB interface. + * @internal + */ +export function isSwatchRGB(value: { [key: string]: any }): value is SwatchRGB { + const test: SwatchRGB = { + r: 0, + g: 0, + b: 0, + toColorString: () => "", + contrast: () => 0, + relativeLuminance: 0, + }; + + for (const key in test) { + if (typeof test[key] !== typeof value[key]) { + return false; + } + } + + return true; +} +/** + * A RGB implementation of {@link Swatch} + * @internal + */ +class SwatchRGBImpl extends ColorRGBA64 implements Swatch { + readonly relativeLuminance: number; + + /** + * + * @param red - Red channel expressed as a number between 0 and 1 + * @param green - Green channel expressed as a number between 0 and 1 + * @param blue - Blue channel expressed as a number between 0 and 1 + */ + constructor(red: number, green: number, blue: number) { + super(red, green, blue, 1); + this.relativeLuminance = rgbToRelativeLuminance(this); + } + + public toColorString = this.toStringHexRGB; + public contrast = contrast.bind(null, this); + public createCSS = this.toColorString; + + static fromObject(obj: { r: number; g: number; b: number }) { + return new SwatchRGBImpl(obj.r, obj.g, obj.b); + } +} diff --git a/packages/components/src/color/utilities/base-layer-luminance.ts b/packages/components/src/color/utilities/base-layer-luminance.ts new file mode 100644 index 00000000..74a0f0d8 --- /dev/null +++ b/packages/components/src/color/utilities/base-layer-luminance.ts @@ -0,0 +1,22 @@ +import { SwatchRGB } from "../swatch.js"; + +export function baseLayerLuminanceSwatch(luminance: number) { + return SwatchRGB.create(luminance, luminance, luminance); +} + +/** + * Recommended values for light and dark mode for {@link @microsoft/fast-components#baseLayerLuminance}. + * + * @public + */ +export const StandardLuminance = { + LightMode: 1, + DarkMode: 0.23, +} as const; + +/** + * Types of recommended values for light and dark mode for {@link @microsoft/fast-components#baseLayerLuminance}. + * + * @public + */ +export type StandardLuminance = typeof StandardLuminance[keyof typeof StandardLuminance]; diff --git a/packages/components/src/color/utilities/binary-search.ts b/packages/components/src/color/utilities/binary-search.ts new file mode 100644 index 00000000..25f3b4cc --- /dev/null +++ b/packages/components/src/color/utilities/binary-search.ts @@ -0,0 +1,31 @@ +/** + * @internal + */ +export function binarySearch( + valuesToSearch: T[] | ReadonlyArray, + searchCondition: (value: T) => boolean, + startIndex: number = 0, + endIndex: number = valuesToSearch.length - 1 +): T { + if (endIndex === startIndex) { + return valuesToSearch[startIndex]; + } + + const middleIndex: number = Math.floor((endIndex - startIndex) / 2) + startIndex; + + // Check to see if this passes on the item in the center of the array + // if it does check the previous values + return searchCondition(valuesToSearch[middleIndex]) + ? binarySearch( + valuesToSearch, + searchCondition, + startIndex, + middleIndex // include this index because it passed the search condition + ) + : binarySearch( + valuesToSearch, + searchCondition, + middleIndex + 1, // exclude this index because it failed the search condition + endIndex + ); +} diff --git a/packages/components/src/color/utilities/color-constants.ts b/packages/components/src/color/utilities/color-constants.ts new file mode 100644 index 00000000..c0561f72 --- /dev/null +++ b/packages/components/src/color/utilities/color-constants.ts @@ -0,0 +1,23 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { SwatchRGB } from "../swatch.js"; + +/** + * @internal + */ +export const white = SwatchRGB.create(1, 1, 1); +/** + * @internal + */ +export const black = SwatchRGB.create(0, 0, 0); + +/** + * @internal + */ +/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ +export const middleGrey = SwatchRGB.from(parseColorHexRGB("#808080")!); + +/** + * @internal + */ +/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ +export const accentBase = SwatchRGB.from(parseColorHexRGB("#DA1A5F")!); diff --git a/packages/components/src/color/utilities/direction-by-is-dark.ts b/packages/components/src/color/utilities/direction-by-is-dark.ts new file mode 100644 index 00000000..fc6b7092 --- /dev/null +++ b/packages/components/src/color/utilities/direction-by-is-dark.ts @@ -0,0 +1,9 @@ +import { Swatch } from "../swatch.js"; +import { isDark } from "./is-dark.js"; + +/** + * @internal + */ +export function directionByIsDark(color: Swatch): 1 | -1 { + return isDark(color) ? -1 : 1; +} diff --git a/packages/components/src/color/utilities/is-dark.ts b/packages/components/src/color/utilities/is-dark.ts new file mode 100644 index 00000000..b09225e4 --- /dev/null +++ b/packages/components/src/color/utilities/is-dark.ts @@ -0,0 +1,20 @@ +import { Swatch } from "../swatch.js"; + +/* + * A color is in "dark" if there is more contrast between #000000 and a reference + * color than #FFFFFF and the reference color. That threshold can be expressed as a relative luminance + * using the contrast formula as (1 + 0.5) / (R + 0.05) === (R + 0.05) / (0 + 0.05), + * which reduces to the following, where 'R' is the relative luminance of the reference color + */ +const target = (-0.1 + Math.sqrt(0.21)) / 2; + +/** + * Determines if a color should be considered Dark Mode + * @param color - The color to check to mode of + * @returns boolean + * + * @public + */ +export function isDark(color: Swatch): boolean { + return color.relativeLuminance <= target; +} diff --git a/packages/components/src/color/utilities/relative-luminance.ts b/packages/components/src/color/utilities/relative-luminance.ts new file mode 100644 index 00000000..115c1faa --- /dev/null +++ b/packages/components/src/color/utilities/relative-luminance.ts @@ -0,0 +1,19 @@ +/** + * @public + */ +export interface RelativeLuminance { + /** + * A number between 0 and 1, calculated by {@link https://www.w3.org/WAI/GL/wiki/Relative_luminance} + */ + readonly relativeLuminance: number; +} + +/** + * @internal + */ +export function contrast(a: RelativeLuminance, b: RelativeLuminance): number { + const L1 = a.relativeLuminance > b.relativeLuminance ? a : b; + const L2 = a.relativeLuminance > b.relativeLuminance ? b : a; + + return (L1.relativeLuminance + 0.05) / (L2.relativeLuminance + 0.05); +} diff --git a/packages/components/src/combobox/combobox.stories.ts b/packages/components/src/combobox/combobox.stories.ts index 8d826294..a0db9754 100644 --- a/packages/components/src/combobox/combobox.stories.ts +++ b/packages/components/src/combobox/combobox.stories.ts @@ -1,122 +1,7 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { getFaIcon, setTheme } from '../utilities/storybook'; -import { Combobox } from './index'; +import Examples from "./fixtures/base.html"; export default { - title: 'Components/Combobox', - argTypes: { - isOpen: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - customIndicator: { control: 'boolean' }, - numberOfChildren: { control: 'number' }, - isMinimal: { control: 'boolean' }, - hasAutoWidth: { control: 'boolean' }, - autocomplete: { - control: 'select', - options: ['none', 'inline', 'list', 'both'] - }, - onChange: { - action: 'changed', - table: { - disable: true - } - } - } -} as Meta; - -const nameList = [ - 'William Hartnell', - 'Patrick Troughton', - 'Jon Pertwee', - 'Tom Baker', - 'Peter Davidson', - 'Colin Baker', - 'Sylvester McCoy', - 'Paul McGann', - 'Christopher Eccleston', - 'David Tenant', - 'Matt Smith', - 'Peter Capaldi', - 'Jodie Whittaker' -]; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.customIndicator ? getFaIcon('sliders-h', 'indicator') : ''} - ${new Array(args.numberOfChildren ?? 10) - .fill(0) - .map((_, index) => { - const name = nameList[index]; - return `${ - name ? name : `Option ${index + 1}` - }`; - }) - .join('\n')} - ` - ); - - const combobox = container.firstChild as Combobox; - - if (args.isOpen) { - combobox.setAttribute('open', ''); - } - - if (args.onChange) { - combobox.addEventListener('change', args.onChange); - } - - return combobox; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - isOpen: false, - isDisabled: false, - customIndicator: false, - numberOfChildren: 10, - isMinimal: false, - hasAutoWidth: false, - autocomplete: 'none', - onChange: action('combobox-onchange') -}; - -export const WithOpen: StoryObj = { render: Template.bind({}) }; -WithOpen.args = { - ...Default.args, - isOpen: true -}; - -export const WithAutoWidth: StoryObj = { render: Template.bind({}) }; -WithAutoWidth.args = { - ...Default.args, - hasAutoWidth: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true + title: "Combobox", }; -export const WithCustomIndicator: StoryObj = { render: Template.bind({}) }; -WithCustomIndicator.args = { - ...Default.args, - customIndicator: true -}; +export const Combobox = () => Examples; diff --git a/packages/components/src/combobox/combobox.styles.ts b/packages/components/src/combobox/combobox.styles.ts index c6dd9892..b3d55e29 100644 --- a/packages/components/src/combobox/combobox.styles.ts +++ b/packages/components/src/combobox/combobox.styles.ts @@ -1,65 +1,53 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - ComboboxOptions, - disabledCursor, - focusVisible, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; + ComboboxOptions, + disabledCursor, + focusVisible, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; import { - accentFillFocus, - focusStrokeWidth, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { selectStyles } from '../select/select.styles'; + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { selectStyles } from "../select/select.styles.js"; /** * Styles for Combobox * @public */ -export const comboboxStyles: FoundationElementTemplate< - ElementStyles, - ComboboxOptions -> = (context, definition) => css` - ${selectStyles(context, definition)} - - :host(:empty) .listbox { - display: none; - } +export const comboboxStyles: FoundationElementTemplate = ( + context, + definition +) => css` + ${selectStyles(context, definition)} - :host([disabled]) *, - :host([disabled]) { - cursor: ${disabledCursor}; - user-select: none; - } + :host(:empty) .listbox { + display: none; + } - :host(:focus-within:not([disabled])) { - border-color: ${accentFillFocus}; - box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) - ${accentFillFocus}; - } + :host([disabled]) *, + :host([disabled]) { + cursor: ${disabledCursor}; + user-select: none; + } - .selected-value { - -webkit-appearance: none; - background: transparent; - border: none; - color: inherit; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - height: calc(100% - (${strokeWidth} * 1px)); - margin: auto 0; - width: 100%; - } + .selected-value { + -webkit-appearance: none; + background: transparent; + border: none; + color: inherit; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + height: calc(100% - (${strokeWidth} * 1px)); + margin: auto 0; + width: 100%; + } - .selected-value:hover, - .selected-value:${focusVisible}, - .selected-value:disabled, - .selected-value:active { - outline: none; - } + .selected-value:hover, + .selected-value:${focusVisible}, + .selected-value:disabled, + .selected-value:active { + outline: none; + } `; diff --git a/packages/components/src/combobox/index.ts b/packages/components/src/combobox/index.ts index f4c5ab8e..12a6b7dc 100644 --- a/packages/components/src/combobox/index.ts +++ b/packages/components/src/combobox/index.ts @@ -1,100 +1,53 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { attr } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; +import type { ComboboxOptions } from "@microsoft/fast-foundation"; import { - Combobox as FoundationCombobox, - ComboboxOptions, - comboboxTemplate as template -} from '@microsoft/fast-foundation'; -import { comboboxStyles as styles } from './combobox.styles'; + Combobox as FoundationCombobox, + comboboxTemplate as template, +} from "@microsoft/fast-foundation"; +import { heightNumberAsToken } from "../design-tokens.js"; +import { comboboxStyles as styles } from "./combobox.styles.js"; /** - * Base class for Select + * Base class for Combobox. * @public */ export class Combobox extends FoundationCombobox { - /** - * Whether the combobox has a compact layout or not. - * - * @public - * @remarks - * HTML Attribute: minimal - */ - @attr({ attribute: 'autowidth', mode: 'boolean' }) - public autoWidth: boolean; - - /** - * Whether the combobox has a compact layout or not. - * - * @public - * @remarks - * HTML Attribute: minimal - */ - @attr({ attribute: 'minimal', mode: 'boolean' }) - public minimal: boolean; - - /** - * The connected callback for this FASTElement. - * - * @override - * - * @internal - */ - connectedCallback(): void { - super.connectedCallback(); - this.setAutoWidth(); - } - - /** - * Synchronize the form-associated proxy and updates the value property of the element. - * - * @param prev - the previous collection of slotted option elements - * @param next - the next collection of slotted option elements - * - * @internal - */ - slottedOptionsChanged(prev: Element[] | undefined, next: Element[]): void { - super.slottedOptionsChanged(prev, next); - this.setAutoWidth(); - } + /** + * An internal stylesheet to hold calculated CSS custom properties. + * + * @internal + */ + private computedStylesheet?: ElementStyles; - /** - * (Un-)set the width when the autoWidth property changes. - * - * @param prev - the previous autoWidth value - * @param next - the current autoWidth value - */ - protected autoWidthChanged(prev: boolean | undefined, next: boolean): void { - if (next) { - this.setAutoWidth(); - } else { - this.style.removeProperty('width'); + /** + * @internal + */ + protected maxHeightChanged(prev: number | undefined, next: number): void { + this.updateComputedStylesheet(); } - } - /** - * Compute the listbox width to set the one of the input. - */ - protected setAutoWidth(): void { - if (!this.autoWidth || !this.isConnected) { - return; - } + /** + * Updates an internal stylesheet with calculated CSS custom properties. + * + * @internal + */ + protected updateComputedStylesheet(): void { + if (this.computedStylesheet) { + this.$fastController.removeStyles(this.computedStylesheet); + } - let listWidth = this.listbox.getBoundingClientRect().width; - // If the list has not been displayed yet trick to get its size - if (listWidth === 0 && this.listbox.hidden) { - Object.assign(this.listbox.style, { visibility: 'hidden' }); - this.listbox.removeAttribute('hidden'); - listWidth = this.listbox.getBoundingClientRect().width; - this.listbox.setAttribute('hidden', ''); - this.listbox.style.removeProperty('visibility'); - } + const popupMaxHeight = Math.floor( + this.maxHeight / heightNumberAsToken.getValueFor(this) + ).toString(); + + this.computedStylesheet = css` + :host { + --listbox-max-height: ${popupMaxHeight}; + } + `; - if (listWidth > 0) { - Object.assign(this.style, { width: `${listWidth}px` }); + this.$fastController.addStyles(this.computedStylesheet); } - } } /** @@ -103,29 +56,29 @@ export class Combobox extends FoundationCombobox { * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * */ -export const jpCombobox = Combobox.compose({ - baseName: 'combobox', - baseClass: FoundationCombobox, - template, - styles, - shadowOptions: { - delegatesFocus: true - }, - indicator: /* html */ ` - - - - ` +export const fastCombobox = Combobox.compose({ + baseName: "combobox", + baseClass: FoundationCombobox, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, + indicator: /* html */ ` + + + + `, }); export { styles as comboboxStyles }; diff --git a/packages/components/src/custom-elements.ts b/packages/components/src/custom-elements.ts index 5c54f482..9d5323c0 100644 --- a/packages/components/src/custom-elements.ts +++ b/packages/components/src/custom-elements.ts @@ -2,90 +2,103 @@ // Distributed under the terms of the Modified BSD License. import type { Container } from '@microsoft/fast-foundation'; -import { jpAccordion } from './accordion/index'; -import { jpAccordionItem } from './accordion-item/index'; -import { jpAnchor } from './anchor/index'; -import { jpAnchoredRegion } from './anchored-region/index'; -import { jpAvatar } from './avatar/index'; -import { jpBadge } from './badge/index'; -import { jpBreadcrumb } from './breadcrumb/index'; -import { jpBreadcrumbItem } from './breadcrumb-item/index'; -import { jpButton } from './button/index'; -import { jpCard } from './card/index'; -import { jpCheckbox } from './checkbox/index'; -import { jpCombobox } from './combobox/index'; -import { jpDataGrid, jpDataGridCell, jpDataGridRow } from './data-grid/index'; -import { jpDateField } from './date-field/index'; -import { jpDialog } from './dialog/index'; -import { jpDivider } from './divider/index'; -import { jpListbox } from './listbox/index'; -import { jpMenu } from './menu/index'; -import { jpMenuItem } from './menu-item/index'; -import { jpNumberField } from './number-field/index'; -import { jpOption } from './option/index'; -import { jpProgress } from './progress/index'; -import { jpProgressRing } from './progress-ring/index'; -import { jpRadio } from './radio/index'; -import { jpRadioGroup } from './radio-group/index'; -import { jpSearch } from './search/index'; -import { jpSelect } from './select/index'; -import { jpSlider } from './slider/index'; -import { jpSliderLabel } from './slider-label/index'; -import { jpSwitch } from './switch/index'; -import { jpTabPanel } from './tab-panel/index'; -import { jpTab } from './tab/index'; -import { jpTabs } from './tabs/index'; -import { jpTextArea } from './text-area/index'; -import { jpTextField } from './text-field/index'; -import { jpToolbar } from './toolbar/index'; -import { jpTooltip } from './tooltip/index'; -import { jpTreeItem } from './tree-item/index'; -import { jpTreeView } from './tree-view/index'; // Don't delete these. They're needed so that API-extractor doesn't add import types // with improper pathing /* eslint-disable @typescript-eslint/no-unused-vars */ -import type { Accordion } from './accordion/index'; -import type { AccordionItem } from './accordion-item/index'; -import type { Anchor } from './anchor/index'; -import type { AnchoredRegion } from './anchored-region/index'; -import type { Avatar } from './avatar/index'; -import type { Badge } from './badge/index'; -import type { Breadcrumb } from './breadcrumb/index'; -import type { BreadcrumbItem } from './breadcrumb-item/index'; -import type { Button } from './button/index'; -import type { Card } from './card/index'; -import type { Checkbox } from './checkbox/index'; -import type { Combobox } from './combobox/index'; -import type { DataGrid, DataGridCell, DataGridRow } from './data-grid/index'; -import type { DateField } from './date-field/index'; -import type { Dialog } from './dialog/index'; -import type { Divider } from './divider/index'; -import type { ListboxElement } from './listbox/index'; -import type { Menu } from './menu/index'; -import type { MenuItem } from './menu-item/index'; -import type { NumberField } from './number-field/index'; -import type { Option } from './option/index'; -import type { Progress } from './progress/index'; -import type { ProgressRing } from './progress-ring/index'; -import type { Radio } from './radio/index'; -import type { RadioGroup } from './radio-group/index'; -import type { Search } from './search/index'; -import type { Select } from './select/index'; -import type { Slider } from './slider/index'; -import type { SliderLabel } from './slider-label/index'; -import type { Switch } from './switch/index'; -import type { TabPanel } from './tab-panel/index'; -import type { Tab } from './tab/index'; -import type { Tabs } from './tabs/index'; -import type { TextArea } from './text-area/index'; -import type { TextField } from './text-field/index'; -import type { Toolbar } from './toolbar/index'; -import type { Tooltip } from './tooltip/index'; -import type { TreeItem } from './tree-item/index'; -import type { TreeView } from './tree-view/index'; +import type { Accordion } from './accordion/index.js'; +import type { AccordionItem } from './accordion-item/index.js'; +import type { Anchor } from './anchor/index.js'; +import type { AnchoredRegion } from './anchored-region/index.js'; +import type { Avatar } from './avatar/index.js'; +import type { Badge } from './badge/index.js'; +import type { Breadcrumb } from './breadcrumb/index.js'; +import type { BreadcrumbItem } from './breadcrumb-item/index.js'; +import type { Button } from './button/index.js'; +import type { Card } from './card/index.js'; +import type { Checkbox } from './checkbox/index.js'; +import type { Combobox } from './combobox/index.js'; +import type { DataGrid, DataGridCell, DataGridRow } from './data-grid/index.js'; +import type { DateField } from './date-field/index.js'; +import type { Dialog } from './dialog/index.js'; +import type { Divider } from './divider/index.js'; +import type { ListboxElement } from './listbox/index.js'; +import type { Menu } from './menu/index.js'; +import type { MenuItem } from './menu-item/index.js'; +import type { NumberField } from './number-field/index.js'; +import type { Option } from './option/index.js'; +import type { Progress } from './progress/index.js'; +import type { ProgressRing } from './progress-ring/index.js'; +import type { Radio } from './radio/index.js'; +import type { RadioGroup } from './radio-group/index.js'; +import type { Search } from './search/index.js'; +import type { Select } from './select/index.js'; +import type { Slider } from './slider/index.js'; +import type { SliderLabel } from './slider-label/index.js'; +import type { Switch } from './switch/index.js'; +import type { TabPanel } from './tab-panel/index.js'; +import type { Tab } from './tab/index.js'; +import type { Tabs } from './tabs/index.js'; +import type { TextArea } from './text-area/index.js'; +import type { TextField } from './text-field/index.js'; +import type { Toolbar } from './toolbar/index.js'; +import type { Tooltip } from './tooltip/index.js'; +import type { TreeItem } from './tree-item/index.js'; +import type { TreeView } from './tree-view/index.js'; + +/** + * Export all custom element definitions + */ + +import { jpAccordion } from './accordion/index.js'; +import { jpAccordionItem } from './accordion-item/index.js'; +import { jpAnchor } from './anchor/index.js'; +import { jpAnchoredRegion } from './anchored-region/index.js'; +import { jpAvatar } from './avatar/index.js'; +import { jpBadge } from './badge/index.js'; +import { jpBreadcrumb } from './breadcrumb/index.js'; +import { jpBreadcrumbItem } from './breadcrumb-item/index.js'; +import { jpButton } from './button/index.js'; +import { jpCard } from './card/index.js'; +import { jpCheckbox } from './checkbox/index.js'; +import { jpCombobox } from './combobox/index.js'; +import { jpDataGrid, jpDataGridCell, jpDataGridRow } from './data-grid/index.js'; +import { jpDateField } from './date-field/index.js'; +/** + * Don't remove. This is needed to prevent api-extractor errors. + */ +// import type { DesignSystemProvider } from "./design-system-provider/index.js"; +import { jpDialog } from './dialog/index.js'; +import { jpDivider } from './divider/index.js'; +import { jpListbox } from './listbox/index.js'; +import { jpMenu } from './menu/index.js'; +import { jpMenuItem } from './menu-item/index.js'; +import { jpNumberField } from './number-field/index.js'; +import { jpOption } from './option/index.js'; +import { jpProgress } from './progress/index.js'; +import { jpProgressRing } from './progress-ring/index.js'; +import { jpRadio } from './radio/index.js'; +import { jpRadioGroup } from './radio-group/index.js'; +import { jpSearch } from './search/index.js'; +import { jpSelect } from './select/index.js'; +import { jpSlider } from './slider/index.js'; +import { jpSliderLabel } from './slider-label/index.js'; +import { jpSwitch } from './switch/index.js'; +import { jpTabPanel } from './tab-panel/index.js'; +import { jpTab } from './tab/index.js'; +import { jpTabs } from './tabs/index.js'; +import { jpTextArea } from './text-area/index.js'; +import { jpTextField } from './text-field/index.js'; +import { jpToolbar } from './toolbar/index.js'; +import { jpTooltip } from './tooltip/index.js'; +import { jpTreeItem } from './tree-item/index.js'; +import { jpTreeView } from './tree-view/index.js'; + +// When adding new components, make sure to add the component to the `allComponents` object +// in addition to exporting the component by name. Ideally we would be able to just add +// `export * as allComponents from "./custom-elements" from src/index.ts but API extractor +// throws for `export * as` expressions. https://github.com/microsoft/rushstack/pull/1796S -// export all components export { jpAccordion, jpAccordionItem, diff --git a/packages/components/src/data-grid/data-grid-cell.styles.ts b/packages/components/src/data-grid/data-grid-cell.styles.ts index 76219d04..e1ce38b7 100644 --- a/packages/components/src/data-grid/data-grid-cell.styles.ts +++ b/packages/components/src/data-grid/data-grid-cell.styles.ts @@ -1,68 +1,68 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; import { - accentFillFocus, - bodyFont, - controlCornerRadius, - designUnit, - focusStrokeWidth, - neutralForegroundRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; + bodyFont, + controlCornerRadius, + designUnit, + focusStrokeOuter, + focusStrokeWidth, + neutralForegroundRest, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; /** * Styles for Data Grid cell * @public */ export const dataGridCellStyles: FoundationElementTemplate = ( - context, - definition + context, + definition ) => - css` - :host { - padding: calc(${designUnit} * 1px) calc(${designUnit} * 3px); - color: ${neutralForegroundRest}; - box-sizing: border-box; - font-family: ${bodyFont}; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - border: transparent calc(${strokeWidth} * 1px) solid; - font-weight: 400; - overflow: hidden; - white-space: nowrap; - border-radius: calc(${controlCornerRadius} * 1px); - } + css` + :host { + padding: calc(${designUnit} * 1px) calc(${designUnit} * 3px); + color: ${neutralForegroundRest}; + box-sizing: border-box; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + font-weight: 400; + border: transparent calc(${focusStrokeWidth} * 1px) solid; + overflow: hidden; + white-space: nowrap; + border-radius: calc(${controlCornerRadius} * 1px); + } - :host(.column-header) { - font-weight: 600; - } + :host(.column-header) { + font-weight: 600; + } - :host(:${focusVisible}) { - outline: calc(${focusStrokeWidth} * 1px) solid ${accentFillFocus}; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host { - forced-color-adjust: none; - border-color: transparent; - background: ${SystemColors.Field}; - color: ${SystemColors.FieldText}; - } + :host(:${focusVisible}) { + border: ${focusStrokeOuter} calc(${focusStrokeWidth} * 1px) solid; + outline: none; + color: ${neutralForegroundRest}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + forced-color-adjust: none; + border-color: transparent; + background: ${SystemColors.Field}; + color: ${SystemColors.FieldText}; + } - :host(:${focusVisible}) { - border-color: ${SystemColors.FieldText}; - box-shadow: 0 0 0 2px inset ${SystemColors.Field}; - } - `) - ); + :host(:${focusVisible}) { + border-color: ${SystemColors.FieldText}; + box-shadow: 0 0 0 2px inset ${SystemColors.Field}; + color: ${SystemColors.FieldText}; + } + ` + ) + ); diff --git a/packages/components/src/data-grid/data-grid-row.styles.ts b/packages/components/src/data-grid/data-grid-row.styles.ts new file mode 100644 index 00000000..3fc4da5a --- /dev/null +++ b/packages/components/src/data-grid/data-grid-row.styles.ts @@ -0,0 +1,33 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { + neutralFillRest, + neutralStrokeDividerRest, + strokeWidth, +} from "../design-tokens.js"; + +/** + * Styles for Data Grid row + * @public + */ +export const dataGridRowStyles: FoundationElementTemplate = ( + context, + definition +) => css` + :host { + display: grid; + padding: 1px 0; + box-sizing: border-box; + width: 100%; + border-bottom: calc(${strokeWidth} * 1px) solid ${neutralStrokeDividerRest}; + } + + :host(.header) { + } + + :host(.sticky-header) { + background: ${neutralFillRest}; + position: sticky; + top: 0; + } +`; diff --git a/packages/components/src/data-grid/data-grid.stories.ts b/packages/components/src/data-grid/data-grid.stories.ts index db319598..568d4766 100644 --- a/packages/components/src/data-grid/data-grid.stories.ts +++ b/packages/components/src/data-grid/data-grid.stories.ts @@ -1,67 +1,490 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import { html } from "@microsoft/fast-element"; +import type { + Button, + ColumnDefinition, + DataGrid, + DataGridCell, + DataGridRow, +} from "@microsoft/fast-foundation"; +import { GenerateHeaderOptions } from "@microsoft/fast-foundation/dist/esm/data-grid/data-grid.options.js"; +import addons from "@storybook/addons"; +import { STORY_RENDERED } from "@storybook/core-events"; +import DataGridTemplate from "./fixtures/base.html"; +import "./index.js"; -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; +let defaultGridElement: DataGrid | null = null; -import { setTheme } from '../utilities/storybook'; -import { DataGrid } from './index'; +const defaultRowData: object = newDataRow("default"); + +const columnWidths: string[] = ["1fr", "1fr", "1fr", "1fr"]; + +const defaultRowItemTemplate = html` + +`; + +const customRowItemTemplate = html` + + +`; + +const customCellItemTemplate = html` + +`; + +const customHeaderCellItemTemplate = html` + +`; + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("data-grid")) { + defaultGridElement = document.getElementById("defaultGrid") as DataGrid; + reset(); + + const nestedCell1 = document.getElementById("nestedCell1") as DataGridCell; + nestedCell1.columnDefinition = nestedColumn; + const nestedCell2 = document.getElementById("nestedCell2") as DataGridCell; + nestedCell2.columnDefinition = nestedColumn; + + const defaultGridRow = document.getElementById("defaultGridRow") as DataGridRow; + if (defaultGridRow) { + defaultGridRow.rowData = defaultRowData; + } + + const defaultRow = document.getElementById("defaultRow") as DataGridRow; + if (defaultRow) { + defaultRow.columnDefinitions = baseColumns; + defaultRow.rowData = defaultRowData; + } + + const defaultHeader = document.getElementById("defaultHeader") as DataGridRow; + if (defaultHeader) { + defaultHeader.columnDefinitions = baseColumns; + } + + const rowWithCellTemplate = document.getElementById( + "cellTemplateRow" + ) as DataGridRow; + if (rowWithCellTemplate) { + rowWithCellTemplate.columnDefinitions = templateColumns; + rowWithCellTemplate.rowData = defaultRowData; + } + + const headerWithCellTemplate = document.getElementById( + "headerTemplateRow" + ) as DataGridRow; + if (headerWithCellTemplate) { + headerWithCellTemplate.columnDefinitions = templateColumns; + } + + const defaultCell = document.getElementById("defaultCell") as DataGridCell; + if (defaultCell) { + defaultCell.columnDefinition = { columnDataKey: "rowId" }; + defaultCell.rowData = defaultRowData; + } + + const headerCell = document.getElementById("headerCell") as DataGridCell; + if (headerCell) { + headerCell.columnDefinition = { + columnDataKey: "name", + title: "Name", + }; + } + + const resetButton = document.getElementById("btnreset") as Button; + if (resetButton) { + resetButton.onclick = reset; + } + + const defaultColsButton = document.getElementById("btndefaultcols") as Button; + if (defaultColsButton) { + defaultColsButton.onclick = setDefaultCols; + } + + const templateColsButton = document.getElementById("btntemplatecols") as Button; + if (templateColsButton) { + templateColsButton.onclick = setTemplateCols; + } + + const addRowButton = document.getElementById("btnaddrow") as Button; + if (addRowButton) { + addRowButton.onclick = addRow; + } + + const removeRowButton = document.getElementById("btnremoverow") as Button; + if (removeRowButton) { + removeRowButton.onclick = removeRow; + } + + const noHeaderButton = document.getElementById("btnnoheader") as Button; + if (noHeaderButton) { + noHeaderButton.onclick = setNoHeader; + } + + const defaultHeaderButton = document.getElementById("btndefaultheader") as Button; + if (defaultHeaderButton) { + defaultHeaderButton.onclick = setDefaultHeader; + } + + const stickyHeaderButton = document.getElementById("btnstickyheader") as Button; + if (stickyHeaderButton) { + stickyHeaderButton.onclick = setStickyHeader; + } + + const defaultRowTemplateButton = document.getElementById( + "btndefaultrowtemplate" + ) as Button; + if (defaultRowTemplateButton) { + defaultRowTemplateButton.onclick = setDefaultRowItemTemplate; + } + + const customRowTemplateButton = document.getElementById( + "btncustomrowtemplate" + ) as Button; + if (customRowTemplateButton) { + customRowTemplateButton.onclick = setCustomRowItemTemplate; + } + + const defaultCellTemplateButton = document.getElementById( + "btndefaultcelltemplate" + ) as Button; + if (defaultCellTemplateButton) { + defaultCellTemplateButton.onclick = setDefaultCellItemTemplate; + } + + const customCellTemplateButton = document.getElementById( + "btncustomcelltemplate" + ) as Button; + if (customCellTemplateButton) { + customCellTemplateButton.onclick = setCustomCellItemTemplate; + } + + const defaultHeaderCellTemplateButton = document.getElementById( + "btndefaultheadercelltemplate" + ) as Button; + if (defaultHeaderCellTemplateButton) { + defaultHeaderCellTemplateButton.onclick = setDefaultHeaderCellItemTemplate; + } + + const customHeaderCellTemplateButton = document.getElementById( + "btncustomheadercelltemplate" + ) as Button; + if (customHeaderCellTemplateButton) { + customHeaderCellTemplateButton.onclick = setCustomHeaderCellItemTemplate; + } + + // note: we use mouse enter because clicking to move focus seems to confuse focus-visible + const focusLeftButton = document.getElementById("btnfocusleft") as Button; + if (focusLeftButton) { + focusLeftButton.onmouseenter = moveFocus; + } + + const focusRightButton = document.getElementById("btnfocusright") as Button; + if (focusRightButton) { + focusRightButton.onmouseenter = moveFocus; + } + + const focusUpButton = document.getElementById("btnfocusup") as Button; + if (focusUpButton) { + focusUpButton.onmouseenter = moveFocus; + } + + const focusDownButton = document.getElementById("btnfocusdown") as Button; + if (focusDownButton) { + focusDownButton.onmouseenter = moveFocus; + } + } +}); + +const buttonCellTemplate = html` + +`; + +const buttonHeaderCellTemplate = html` + +`; + +function reset(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.columnDefinitions = null; + defaultGridElement.rowsData = newDataSet(10); +} + +function setDefaultCols(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.columnDefinitions = baseColumns; +} + +function setTemplateCols(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.columnDefinitions = templateColumns; +} + +function addRow(): void { + if (defaultGridElement === null || defaultGridElement.rowsData === null) { + return; + } + defaultGridElement.rowsData.push( + newDataRow(`${defaultGridElement.rowsData.length + 1}`) + ); +} + +function removeRow(): void { + if ( + defaultGridElement === null || + defaultGridElement.rowsData === null || + defaultGridElement.rowsData.length === 0 + ) { + return; + } + defaultGridElement.rowsData.pop(); +} + +function setNoHeader(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.generateHeader = GenerateHeaderOptions.none; +} + +function setDefaultHeader(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.generateHeader = GenerateHeaderOptions.default; +} + +function setDefaultRowItemTemplate(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.rowItemTemplate = defaultRowItemTemplate; +} + +function setCustomRowItemTemplate(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.rowItemTemplate = customRowItemTemplate; +} + +function setDefaultCellItemTemplate(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.cellItemTemplate = undefined; +} + +function setCustomCellItemTemplate(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.cellItemTemplate = customCellItemTemplate; +} + +function setDefaultHeaderCellItemTemplate(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.headerCellItemTemplate = undefined; +} + +function setCustomHeaderCellItemTemplate(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.headerCellItemTemplate = customHeaderCellItemTemplate; +} + +function moveFocus(e: MouseEvent): void { + if (defaultGridElement === null) { + return; + } + switch ((e.target as HTMLElement).id) { + case "btnfocusleft": + defaultGridElement.focusColumnIndex = defaultGridElement.focusColumnIndex - 1; + break; + + case "btnfocusright": + defaultGridElement.focusColumnIndex = defaultGridElement.focusColumnIndex + 1; + break; + + case "btnfocusup": + defaultGridElement.focusRowIndex = defaultGridElement.focusRowIndex - 1; + break; + + case "btnfocusdown": + defaultGridElement.focusRowIndex = defaultGridElement.focusRowIndex + 1; + break; + } +} + +function headerTemplateButtonClick(cell: DataGridCell): void { + if ( + cell.columnDefinition === null || + defaultGridElement === null || + defaultGridElement.columnDefinitions === null + ) { + return; + } + + const index: number = defaultGridElement.columnDefinitions.indexOf( + cell.columnDefinition + ); + + if (columnWidths[index] === "1fr") { + columnWidths.splice(index, 1, "2fr"); + } else { + columnWidths.splice(index, 1, "1fr"); + } + + defaultGridElement.gridTemplateColumns = `${columnWidths[0]} ${columnWidths[1]} ${columnWidths[2]} ${columnWidths[3]}`; +} + +function cellTemplateButtonClick(cell: DataGridCell): void { + if ( + cell.columnDefinition === null || + cell.rowData === null || + defaultGridElement === null + ) { + return; + } + const newRowData: object = { ...cell.rowData }; + newRowData[cell.columnDefinition.columnDataKey] = "clicked"; + + const rowIndex: number = defaultGridElement.rowsData.indexOf(cell.rowData); + + if (rowIndex > -1) { + defaultGridElement.rowsData.splice(rowIndex, 1, newRowData); + } +} + +function setStickyHeader(): void { + if (defaultGridElement === null) { + return; + } + defaultGridElement.generateHeader = GenerateHeaderOptions.sticky; +} + +function newDataSet(rowCount: number): object[] { + const newRows: object[] = []; + for (let i = 0; i <= rowCount; i++) { + newRows.push(newDataRow(`${i + 1}`)); + } + return newRows; +} + +function newDataRow(id: string): object { + return { + rowId: `rowid-${id}`, + item1: `value 1-${id}`, + item2: `value 2-${id}`, + item3: `value 3-${id}`, + item4: `value 4-${id}`, + item5: `value 5-${id}`, + item6: `value 6-${id}`, + }; +} + +const baseColumns: ColumnDefinition[] = [ + { columnDataKey: "rowId", isRowHeader: true }, + { columnDataKey: "item1" }, + { columnDataKey: "item2" }, + { columnDataKey: "item3" }, +]; + +const nestedColumn: ColumnDefinition = { + columnDataKey: "item2", + cellInternalFocusQueue: true, + cellFocusTargetCallback: getFocusTarget, +}; + +const templateColumns: ColumnDefinition[] = [ + { + title: "RowID", + isRowHeader: true, + columnDataKey: "rowId", + cellTemplate: buttonCellTemplate, + cellFocusTargetCallback: getFocusTarget, + headerCellTemplate: buttonHeaderCellTemplate, + headerCellFocusTargetCallback: getFocusTarget, + }, + { + title: "Column 1", + columnDataKey: "item1", + cellTemplate: buttonCellTemplate, + cellFocusTargetCallback: getFocusTarget, + headerCellTemplate: buttonHeaderCellTemplate, + headerCellFocusTargetCallback: getFocusTarget, + }, + { + title: "Column 2", + columnDataKey: "item2", + cellTemplate: buttonCellTemplate, + cellFocusTargetCallback: getFocusTarget, + headerCellTemplate: buttonHeaderCellTemplate, + headerCellFocusTargetCallback: getFocusTarget, + }, + { + title: "Column 3", + columnDataKey: "item3", + cellTemplate: buttonCellTemplate, + cellFocusTargetCallback: getFocusTarget, + headerCellTemplate: buttonHeaderCellTemplate, + headerCellFocusTargetCallback: getFocusTarget, + }, +]; + +function getFocusTarget(cell: DataGridCell): HTMLElement { + return cell.children[0] as HTMLElement; +} export default { - title: 'Components/Data Grid', - argTypes: {}, - parameters: { - controls: { disabled: true }, - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - // return ` - // - // 1.1 - // 1.2 - // - // - // 2.1 - // 2.2 - // - // `; - - setTimeout(() => { - const grid = document.getElementById('basic-grid') as DataGrid | null; - - if (grid) { - grid.rowsData = [ - { - Header1: 'Data 1 1', - Header2: 'Data 2 1', - Header3: 'Data 3 1', - Header4: 'Cell Data 4 1' - }, - { - Header1: 'Data 1 2', - Header2: 'Data 2 2', - Header3: 'Data 3 2', - Header4: 'Cell Data 4 2' - }, - { - Header1: 'Data 1 3', - Header2: 'Data 2 3', - Header3: 'Data 3 3', - Header4: 'Cell Data 4 3' - } - ]; - } - }, 0); - - return ''; + title: "Data Grid", }; -export const Default: StoryObj = { render: Template.bind({}) }; +export const dataGrid = () => DataGridTemplate; diff --git a/packages/components/src/data-grid/data-grid.styles.ts b/packages/components/src/data-grid/data-grid.styles.ts new file mode 100644 index 00000000..82976b68 --- /dev/null +++ b/packages/components/src/data-grid/data-grid.styles.ts @@ -0,0 +1,17 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { FoundationElementTemplate } from "@microsoft/fast-foundation"; + +/** + * Styles for Data Grid + * @public + */ +export const dataGridStyles: FoundationElementTemplate = ( + context, + definition +) => css` + :host { + display: flex; + position: relative; + flex-direction: column; + } +`; diff --git a/packages/components/src/data-grid/index.ts b/packages/components/src/data-grid/index.ts index 1b0251d3..c495405d 100644 --- a/packages/components/src/data-grid/index.ts +++ b/packages/components/src/data-grid/index.ts @@ -1,28 +1,26 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - DataGrid, - DataGridCell, - dataGridCellTemplate, - DataGridRow, - dataGridRowTemplate, - dataGridTemplate -} from '@microsoft/fast-foundation'; -import { dataGridStyles, dataGridRowStyles } from '@microsoft/fast-components'; -import { dataGridCellStyles } from './data-grid-cell.styles'; + DataGrid, + DataGridCell, + dataGridCellTemplate, + DataGridRow, + dataGridRowTemplate, + dataGridTemplate, +} from "@microsoft/fast-foundation"; +import { dataGridStyles } from "./data-grid.styles.js"; +import { dataGridRowStyles } from "./data-grid-row.styles.js"; +import { dataGridCellStyles } from "./data-grid-cell.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#DataGridCell} registration for configuring the component with a DesignSystem. * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpDataGridCell = DataGridCell.compose({ - baseName: 'data-grid-cell', - template: dataGridCellTemplate, - styles: dataGridCellStyles +export const fastDataGridCell = DataGridCell.compose({ + baseName: "data-grid-cell", + template: dataGridCellTemplate, + styles: dataGridCellStyles, }); /** @@ -30,12 +28,12 @@ export const jpDataGridCell = DataGridCell.compose({ * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpDataGridRow = DataGridRow.compose({ - baseName: 'data-grid-row', - template: dataGridRowTemplate, - styles: dataGridRowStyles +export const fastDataGridRow = DataGridRow.compose({ + baseName: "data-grid-row", + template: dataGridRowTemplate, + styles: dataGridRowStyles, }); /** @@ -43,12 +41,12 @@ export const jpDataGridRow = DataGridRow.compose({ * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpDataGrid = DataGrid.compose({ - baseName: 'data-grid', - template: dataGridTemplate, - styles: dataGridStyles +export const fastDataGrid = DataGrid.compose({ + baseName: "data-grid", + template: dataGridTemplate, + styles: dataGridStyles, }); /** diff --git a/packages/components/src/design-system-provider/README.md b/packages/components/src/design-system-provider/README.md new file mode 100644 index 00000000..77e0effa --- /dev/null +++ b/packages/components/src/design-system-provider/README.md @@ -0,0 +1,68 @@ +# Design System Provider +For more information view the [design system provider readme](../../../fast-foundation/src/design-system-provider/README.md). + +### FAST Design System Properties +|Property Name|Type|Attribute Name|CSS Custom property| +|---|---|---|---| +|fillColor|string| fill-color | fill-color | +|neutralPalette|string[]| N/A | N/A | +|accentPalette|string[]| N/A | N/A | +|density|DensityOffset| number | density | +|designUnit|number| number | design-unit | +|baseHeightMultiplier|number| base-height-multiplier | base-height-multiplier | +|baseHorizontalSpacingMultiplier|number| base-horizontal-spacing-multiplier | base-horizontal-spacing-multiplier | +|controlCornerRadius|number| corner-radius | corner-radius | +|strokeWidth|number| stroke-width | stroke-width | +|focusStrokeWidth|number| focus-stroke-width | focus-stroke-width | +|disabledOpacity|number| disabled-opacity | disabled-opacity | +|typeRampMinus2FontSize | string | type-ramp-minus-2-font-size | type-ramp-minus-2-font-size | +|typeRampMinus2LineHeight | string | type-ramp-minus-2-line-height | type-ramp-minus-2-line-height | +|typeRampMinus1FontSize | string | type-ramp-minus-1-font-size | type-ramp-minus-1-font-size | +|typeRampMinus1LineHeight | string | type-ramp-minus-1-line-height | type-ramp-minus-1-line-height | +|typeRampBaseFontSize | string | type-ramp-base-font-size | type-ramp-base-font-size | +|typeRampBaseLineHeight | string | type-ramp-base-line-height | type-ramp-base-line-height | +|typeRampPlus1FontSize | string | type-ramp-plus-1-font-size | type-ramp-plus-1-font-size | +|typeRampPlus1LineHeight | string | type-ramp-plus-1-line-height | type-ramp-plus-1-line-height | +|typeRampPlus2FontSize | string | type-ramp-plus-2-font-size | type-ramp-plus-2-font-size | +|typeRampPlus2LineHeight | string | type-ramp-plus-2-line-height | type-ramp-plus-2-line-height | +|typeRampPlus3FontSize | string | type-ramp-plus-3-font-size | type-ramp-plus-3-font-size | +|typeRampPlus3LineHeight | string | type-ramp-plus-3-line-height | type-ramp-plus-3-line-height | +|typeRampPlus4FontSize | string | type-ramp-plus-4-font-size | type-ramp-plus-4-font-size | +|typeRampPlus4LineHeight | string | type-ramp-plus-4-line-height | type-ramp-plus-4-line-height | +|typeRampPlus5FontSize | string | type-ramp-plus-5-font-size | type-ramp-plus-5-font-size | +|typeRampPlus5LineHeight | string | type-ramp-plus-5-line-height | type-ramp-plus-5-line-height | +|typeRampPlus6FontSize | string | type-ramp-plus-6-font-size | type-ramp-plus-6-font-size | +|typeRampPlus6LineHeight | string | type-ramp-plus-6-line-height | type-ramp-plus-6-line-height | +|accentFillRestDelta|number| accent-fill-rest-delta | N/A | +|accentFillHoverDelta|number| accent-fill-hover-delta | N/A | +|accentFillActiveDelta|number| accent-fill-active-delta | N/A | +|accentFillFocusDelta|number| accent-fill-focus-delta | N/A | +|accentForegroundRestDelta|number| accent-foreground-rest-delta | N/A | +|accentForegroundHoverDelta|number| accent-foreground-hover-delta | N/A | +|accentForegroundActiveDelta|number| accent-foreground-active-delta | N/A | +|accentForegroundFocusDelta|number| accent-foreground-focus-delta | N/A | +|neutralFillRestDelta|number| neutral-fill-rest-delta | N/A | +|neutralFillHoverDelta|number| neutral-fill-hover-delta | N/A | +|neutralFillActiveDelta|number| neutral-fill-active-delta | N/A | +|neutralFillFocusDelta|number| neutral-fill-focus-delta | N/A | +|neutralFillInputRestDelta|number| neutral-fill-input-rest-delta | N/A | +|neutralFillInputHoverDelta|number| neutral-fill-input-hover-delta | N/A | +|neutralFillInputActiveDelta|number| neutral-fill-input-active-delta | N/A | +|neutralFillInputFocusDelta|number| neutral-fill-input-focus-delta | N/A | +|neutralFillStealthRestDelta|number| neutral-fill-stealth-rest-delta | N/A | +|neutralFillStealthHoverDelta|number| neutral-fill-stealth-hover-delta | N/A | +|neutralFillStealthActiveDelta|number| neutral-fill-stealth-active-delta | N/A | +|neutralFillStealthFocusDelta|number| neutral-fill-stealth-focus-delta | N/A | +|neutralFillStrongHoverDelta|number| neutral-fill-strong-hover-delta | N/A | +|neutralFillStrongActiveDelta|number| neutral-fill-strong-hover-active | N/A | +|neutralFillStrongFocusDelta|number| neutral-fill-strong-hover-focus | N/A | +|baseLayerLuminance|number base-layer-luminance| | N/A | +|neutralFillLayerRestDelta|number| neutral-fill-layer-rest-delta | N/A | +|neutralForegroundHoverDelta|number| neutral-foreground-hover-delta | N/A | +|neutralForegroundActiveDelta|number| neutral-foreground-active-delta | N/A | +|neutralForegroundFocusDelta|number| neutral-foreground-focus-delta | N/A | +|neutralStrokeDividerRestDelta|number| neutral-stroke-divider-rest-delta | N/A | +|neutralStrokeRestDelta|number| neutral-stroke-rest-delta | N/A | +|neutralStrokeHoverDelta|number| neutral-stroke-hover-delta | N/A | +|neutralStrokeActiveDelta|number| neutral-stroke-active-delta | N/A | +|neutralStrokeFocusDelta|number| neutral-stroke-focus-delta | N/A | diff --git a/packages/components/src/design-system-provider/index.ts b/packages/components/src/design-system-provider/index.ts new file mode 100644 index 00000000..56e5b6d7 --- /dev/null +++ b/packages/components/src/design-system-provider/index.ts @@ -0,0 +1,1087 @@ +import { parseColorHexRGB } from "@microsoft/fast-colors"; +import { + attr, + css, + html, + nullableNumberConverter, + Observable, + ValueConverter, +} from "@microsoft/fast-element"; +import { + DesignToken, + DesignTokenValue, + display, + ElementDefinitionContext, + forcedColorsStylesheetBehavior, + FoundationElement, + FoundationElementDefinition, +} from "@microsoft/fast-foundation"; +import { Direction, SystemColors } from "@microsoft/fast-web-utilities"; +import { Swatch, SwatchRGB } from "../color/swatch.js"; +import { + accentColor, + accentFillActiveDelta, + accentFillFocusDelta, + accentFillHoverDelta, + accentFillRestDelta, + accentForegroundActiveDelta, + accentForegroundFocusDelta, + accentForegroundHoverDelta, + accentForegroundRestDelta, + baseHeightMultiplier, + baseHorizontalSpacingMultiplier, + baseLayerLuminance, + controlCornerRadius, + density, + designUnit, + direction, + disabledOpacity, + fillColor, + focusStrokeWidth, + neutralColor, + neutralFillActiveDelta, + neutralFillFocusDelta, + neutralFillHoverDelta, + neutralFillInputActiveDelta, + neutralFillInputFocusDelta, + neutralFillInputHoverDelta, + neutralFillInputRestDelta, + neutralFillLayerRestDelta, + neutralFillRestDelta, + neutralFillStealthActiveDelta, + neutralFillStealthFocusDelta, + neutralFillStealthHoverDelta, + neutralFillStealthRestDelta, + neutralFillStrongActiveDelta, + neutralFillStrongFocusDelta, + neutralFillStrongHoverDelta, + neutralForegroundRest, + neutralStrokeActiveDelta, + neutralStrokeDividerRestDelta, + neutralStrokeFocusDelta, + neutralStrokeHoverDelta, + neutralStrokeRestDelta, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, + typeRampMinus1FontSize, + typeRampMinus1LineHeight, + typeRampMinus2FontSize, + typeRampMinus2LineHeight, + typeRampPlus1FontSize, + typeRampPlus1LineHeight, + typeRampPlus2FontSize, + typeRampPlus2LineHeight, + typeRampPlus3FontSize, + typeRampPlus3LineHeight, + typeRampPlus4FontSize, + typeRampPlus4LineHeight, + typeRampPlus5FontSize, + typeRampPlus5LineHeight, + typeRampPlus6FontSize, + typeRampPlus6LineHeight, +} from "../design-tokens.js"; + +/** + * A {@link ValueConverter} that converts to and from `Swatch` values. + * @remarks + * This converter allows for colors represented as string hex values, returning `null` if the + * input was `null` or `undefined`. + * @internal + */ +const swatchConverter: ValueConverter = { + toView(value: any): string | null { + if (value === null || value === undefined) { + return null; + } + return (value as Swatch)?.toColorString(); + }, + + fromView(value: any): any { + if (value === null || value === undefined) { + return null; + } + const color = parseColorHexRGB(value); + return color ? SwatchRGB.create(color.r, color.g, color.b) : null; + }, +}; + +const backgroundStyles = css` + :host { + background-color: ${fillColor}; + color: ${neutralForegroundRest}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + background-color: ${SystemColors.ButtonFace}; + box-shadow: 0 0 0 1px ${SystemColors.CanvasText}; + color: ${SystemColors.ButtonText}; + } + ` + ) +); + +function designToken(token: DesignToken) { + return (source: DesignSystemProvider, key: string) => { + source[key + "Changed"] = function ( + this: DesignSystemProvider, + prev: T | undefined, + next: T | undefined + ) { + if (next !== undefined && next !== null) { + token.setValueFor(this, next as DesignTokenValue); + } else { + token.deleteValueFor(this); + } + }; + }; +} + +/** + * The FAST DesignSystemProvider Element. + * @internal + */ +export class DesignSystemProvider extends FoundationElement { + constructor() { + super(); + + // If fillColor or baseLayerLuminance change, we need to + // re-evaluate whether we should have paint styles applied + const subscriber = { + handleChange: this.noPaintChanged.bind(this), + }; + Observable.getNotifier(this).subscribe(subscriber, "fillColor"); + Observable.getNotifier(this).subscribe(subscriber, "baseLayerLuminance"); + } + /** + * Used to instruct the FASTDesignSystemProvider + * that it should not set the CSS + * background-color and color properties + * + * @remarks + * HTML boolean attribute: no-paint + */ + @attr({ attribute: "no-paint", mode: "boolean" }) + public noPaint = false; + private noPaintChanged() { + if (!this.noPaint && (this.fillColor !== void 0 || this.baseLayerLuminance)) { + this.$fastController.addStyles(backgroundStyles); + } else { + this.$fastController.removeStyles(backgroundStyles); + } + } + + /** + * Define design system property attributes + * @remarks + * HTML attribute: background-color + * + * CSS custom property: --fill-color + */ + @attr({ + attribute: "fill-color", + converter: swatchConverter, + }) + @designToken(fillColor) + public fillColor?: Swatch; + + /** + * Set the accent color + * @remarks + * HTML attribute: accent-color + */ + @attr({ + attribute: "accent-color", + converter: swatchConverter, + mode: "fromView", + }) + @designToken(accentColor) + public accentColor?: Swatch; + + /** + * Set the neutral color + * @remarks + * HTML attribute: neutral-color + */ + @attr({ + attribute: "neutral-color", + converter: swatchConverter, + mode: "fromView", + }) + @designToken(neutralColor) + public neutralColor?: Swatch; + + /** + * + * The density offset, used with designUnit to calculate height and spacing. + * + * @remarks + * HTML attribute: density + * + * CSS custom property: --density + */ + @attr({ + converter: nullableNumberConverter, + }) + @designToken(density) + public density?: number; + + /** + * The grid-unit that UI dimensions are derived from in pixels. + * + * @remarks + * HTML attribute: design-unit + * + * CSS custom property: --design-unit + */ + @attr({ + attribute: "design-unit", + converter: nullableNumberConverter, + }) + @designToken(designUnit) + public designUnit?: number; + + /** + * The primary document direction. + * + * @remarks + * HTML attribute: direction + * + * CSS custom property: N/A + */ + @attr({ + attribute: "direction", + }) + @designToken(direction) + public direction?: Direction; + + /** + * The number of designUnits used for component height at the base density. + * + * @remarks + * HTML attribute: base-height-multiplier + * + * CSS custom property: --base-height-multiplier + */ + @attr({ + attribute: "base-height-multiplier", + converter: nullableNumberConverter, + }) + @designToken(baseHeightMultiplier) + public baseHeightMultiplier?: number; + + /** + * The number of designUnits used for horizontal spacing at the base density. + * + * @remarks + * HTML attribute: base-horizontal-spacing-multiplier + * + * CSS custom property: --base-horizontal-spacing-multiplier + */ + @attr({ + attribute: "base-horizontal-spacing-multiplier", + converter: nullableNumberConverter, + }) + @designToken(baseHorizontalSpacingMultiplier) + public baseHorizontalSpacingMultiplier?: number; + + /** + * The corner radius applied to controls. + * + * @remarks + * HTML attribute: control-corner-radius + * + * CSS custom property: --control-corner-radius + */ + @attr({ + attribute: "control-corner-radius", + converter: nullableNumberConverter, + }) + @designToken(controlCornerRadius) + public controlCornerRadius?: number; + + /** + * The width of the standard stroke applied to stroke components in pixels. + * + * @remarks + * HTML attribute: stroke-width + * + * CSS custom property: --stroke-width + */ + @attr({ + attribute: "stroke-width", + converter: nullableNumberConverter, + }) + @designToken(strokeWidth) + public strokeWidth?: number; + + /** + * The width of the standard focus stroke in pixels. + * + * @remarks + * HTML attribute: focus-stroke-width + * + * CSS custom property: --focus-stroke-width + */ + @attr({ + attribute: "focus-stroke-width", + converter: nullableNumberConverter, + }) + @designToken(focusStrokeWidth) + public focusStrokeWidth?: number; + + /** + * The opacity of a disabled control. + * + * @remarks + * HTML attribute: disabled-opacity + * + * CSS custom property: --disabled-opacity + */ + @attr({ + attribute: "disabled-opacity", + converter: nullableNumberConverter, + }) + @designToken(disabledOpacity) + public disabledOpacity?: number; + + /** + * The font-size two steps below the base font-size + * + * @remarks + * HTML attribute: type-ramp-minus-2-font-size + * + * CSS custom property: --type-ramp-minus-2-font-size + */ + @attr({ + attribute: "type-ramp-minus-2-font-size", + }) + @designToken(typeRampMinus2FontSize) + public typeRampMinus2FontSize?: string; + + /** + * The line-height two steps below the base line-height + * + * @remarks + * HTML attribute: type-ramp-minus-2-line-height + * + * CSS custom property: --type-ramp-minus-2-line-height + */ + @attr({ + attribute: "type-ramp-minus-2-line-height", + }) + @designToken(typeRampMinus2LineHeight) + public typeRampMinus2LineHeight?: string; + + /** + * The font-size one step below the base font-size + * + * @remarks + * HTML attribute: type-ramp-minus-1-font-size + * + * CSS custom property: --type-ramp-minus-1-font-size + */ + @attr({ + attribute: "type-ramp-minus-1-font-size", + }) + @designToken(typeRampMinus1FontSize) + public typeRampMinus1FontSize?: string; + + /** + * The line-height one step below the base line-height + * + * @remarks + * HTML attribute: type-ramp-minus-1-line-height + * + * CSS custom property: --type-ramp-minus-1-line-height + */ + @attr({ + attribute: "type-ramp-minus-1-line-height", + }) + @designToken(typeRampMinus1LineHeight) + public typeRampMinus1LineHeight?: string; + + /** + * The base font-size of the relative type-ramp scale + * + * @remarks + * HTML attribute: type-ramp-base-font-size + * + * CSS custom property: --type-ramp-base-font-size + */ + @attr({ + attribute: "type-ramp-base-font-size", + }) + @designToken(typeRampBaseFontSize) + public typeRampBaseFontSize?: string; + + /** + * The base line-height of the relative type-ramp scale + * + * @remarks + * HTML attribute: type-ramp-base-line-height + * + * CSS custom property: --type-ramp-base-line-height + */ + @attr({ + attribute: "type-ramp-base-line-height", + }) + @designToken(typeRampBaseLineHeight) + public typeRampBaseLineHeight?: string; + + /** + * The font-size one step above the base font-size + * + * @remarks + * HTML attribute: type-ramp-plus-1-font-size + * + * CSS custom property: --type-ramp-plus-1-font-size + */ + @attr({ + attribute: "type-ramp-plus-1-font-size", + }) + @designToken(typeRampPlus1FontSize) + public typeRampPlus1FontSize?: string; + + /** + * The line-height one step above the base line-height + * + * @remarks + * HTML attribute: type-ramp-plus-1-line-height + * + * CSS custom property: --type-ramp-plus-1-line-height + */ + @attr({ + attribute: "type-ramp-plus-1-line-height", + }) + @designToken(typeRampPlus1LineHeight) + public typeRampPlus1LineHeight?: string; + + /** + * The font-size two steps above the base font-size + * + * @remarks + * HTML attribute: type-ramp-plus-2-font-size + * + * CSS custom property: --type-ramp-plus-2-font-size + */ + @attr({ + attribute: "type-ramp-plus-2-font-size", + }) + @designToken(typeRampPlus2FontSize) + public typeRampPlus2FontSize?: string; + + /** + * The line-height two steps above the base line-height + * + * @remarks + * HTML attribute: type-ramp-plus-2-line-height + * + * CSS custom property: --type-ramp-plus-2-line-height + */ + @attr({ + attribute: "type-ramp-plus-2-line-height", + }) + @designToken(typeRampPlus2LineHeight) + public typeRampPlus2LineHeight?: string; + + /** + * The font-size three steps above the base font-size + * + * @remarks + * HTML attribute: type-ramp-plus-3-font-size + * + * CSS custom property: --type-ramp-plus-3-font-size + */ + @attr({ + attribute: "type-ramp-plus-3-font-size", + }) + @designToken(typeRampPlus3FontSize) + public typeRampPlus3FontSize?: string; + + /** + * The line-height three steps above the base line-height + * + * @remarks + * HTML attribute: type-ramp-plus-3-line-height + * + * CSS custom property: --type-ramp-plus-3-line-height + */ + @attr({ + attribute: "type-ramp-plus-3-line-height", + }) + @designToken(typeRampPlus3LineHeight) + public typeRampPlus3LineHeight?: string; + + /** + * The font-size four steps above the base font-size + * + * @remarks + * HTML attribute: type-ramp-plus-4-font-size + * + * CSS custom property: --type-ramp-plus-4-font-size + */ + @attr({ + attribute: "type-ramp-plus-4-font-size", + }) + @designToken(typeRampPlus4FontSize) + public typeRampPlus4FontSize?: string; + + /** + * The line-height four steps above the base line-height + * + * @remarks + * HTML attribute: type-ramp-plus-4-line-height + * + * CSS custom property: --type-ramp-plus-4-line-height + */ + @attr({ + attribute: "type-ramp-plus-4-line-height", + }) + @designToken(typeRampPlus4LineHeight) + public typeRampPlus4LineHeight?: string; + + /** + * The font-size five steps above the base font-size + * + * @remarks + * HTML attribute: type-ramp-plus-5-font-size + * + * CSS custom property: --type-ramp-plus-5-font-size + */ + @attr({ + attribute: "type-ramp-plus-5-font-size", + }) + @designToken(typeRampPlus5FontSize) + public typeRampPlus5FontSize?: string; + + /** + * The line-height five steps above the base line-height + * + * @remarks + * HTML attribute: type-ramp-plus-5-line-height + * + * CSS custom property: --type-ramp-plus-5-line-height + */ + @attr({ + attribute: "type-ramp-plus-5-line-height", + }) + @designToken(typeRampPlus5LineHeight) + public typeRampPlus5LineHeight?: string; + + /** + * The font-size six steps above the base font-size + * + * @remarks + * HTML attribute: type-ramp-plus-6-font-size + * + * CSS custom property: --type-ramp-plus-6-font-size + */ + @attr({ + attribute: "type-ramp-plus-6-font-size", + }) + @designToken(typeRampPlus6FontSize) + public typeRampPlus6FontSize?: string; + + /** + * The line-height six steps above the base line-height + * + * @remarks + * HTML attribute: type-ramp-plus-6-line-height + * + * CSS custom property: --type-ramp-plus-6-line-height + */ + @attr({ + attribute: "type-ramp-plus-6-line-height", + }) + @designToken(typeRampPlus6LineHeight) + public typeRampPlus6LineHeight?: string; + + /** + * The distance from the resolved accent fill color for the rest state of the accent-fill recipe. See {@link @microsoft/fast-components#accentFillRest} for usage in CSS. + * + * @remarks + * HTML attribute: accent-fill-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-fill-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(accentFillRestDelta) + public accentFillRestDelta?: number; + + /** + * The distance from the resolved accent fill color for the hover state of the accent-fill recipe. See {@link @microsoft/fast-components#accentFillHover} for usage in CSS. + * + * @remarks + * HTML attribute: accent-fill-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-fill-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(accentFillHoverDelta) + public accentFillHoverDelta?: number; + + /** + * The distance from the resolved accent fill color for the active state of the accent-fill recipe. See {@link @microsoft/fast-components#accentFillActive} for usage in CSS. + * + * @remarks + * HTML attribute: accent-fill-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-fill-active-delta", + converter: nullableNumberConverter, + }) + @designToken(accentFillActiveDelta) + public accentFillActiveDelta?: number; + + /** + * The distance from the resolved accent fill color for the focus state of the accent-fill recipe. See {@link @microsoft/fast-components#accentFillFocus} for usage in CSS. + * + * @remarks + * HTML attribute: accent-fill-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-fill-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(accentFillFocusDelta) + public accentFillFocusDelta?: number; + + /** + * The distance from the resolved accent foreground color for the rest state of the accent-foreground recipe. See {@link @microsoft/fast-components#accentForegroundRest} for usage in CSS. + * + * @remarks + * HTML attribute: accent-foreground-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-foreground-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(accentForegroundRestDelta) + public accentForegroundRestDelta?: number; + + /** + * The distance from the resolved accent foreground color for the hover state of the accent-foreground recipe. See {@link @microsoft/fast-components#accentForegroundHover} for usage in CSS. + * + * @remarks + * HTML attribute: accent-foreground-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-foreground-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(accentForegroundHoverDelta) + public accentForegroundHoverDelta?: number; + + /** + * The distance from the resolved accent foreground color for the active state of the accent-foreground recipe. See {@link @microsoft/fast-components#accentForegroundActive} for usage in CSS. + * + * @remarks + * HTML attribute: accent-foreground-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-foreground-active-delta", + converter: nullableNumberConverter, + }) + @designToken(accentForegroundActiveDelta) + public accentForegroundActiveDelta?: number; + + /** + * The distance from the resolved accent foreground color for the focus state of the accent-foreground recipe. See {@link @microsoft/fast-components#accentForegroundFocus} for usage in CSS. + * + * @remarks + * HTML attribute: accent-foreground-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "accent-foreground-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(accentForegroundFocusDelta) + public accentForegroundFocusDelta?: number; + + /** + * The distance from the resolved neutral fill color for the rest state of the neutral-fill recipe. See {@link @microsoft/fast-components#neutralFillRest} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillRestDelta) + public neutralFillRestDelta?: number; + + /** + * The distance from the resolved neutral fill color for the hover state of the neutral-fill recipe. See {@link @microsoft/fast-components#neutralFillHover} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillHoverDelta) + public neutralFillHoverDelta?: number; + + /** + * The distance from the resolved neutral fill color for the active state of the neutral-fill recipe. See {@link @microsoft/fast-components#neutralFillActive} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-active-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillActiveDelta) + public neutralFillActiveDelta?: number; + + /** + * The distance from the resolved neutral fill color for the focus state of the neutral-fill recipe. See {@link @microsoft/fast-components#neutralFillFocus} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillFocusDelta) + public neutralFillFocusDelta?: number; + + /** + * The distance from the resolved neutral fill input color for the rest state of the neutral-fill-input recipe. See {@link @microsoft/fast-components#neutralFillInputRest} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-input-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-input-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillInputRestDelta) + public neutralFillInputRestDelta?: number; + + /** + * The distance from the resolved neutral fill input color for the hover state of the neutral-fill-input recipe. See {@link @microsoft/fast-components#neutralFillInputHover} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-input-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-input-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillInputHoverDelta) + public neutralFillInputHoverDelta?: number; + + /** + * The distance from the resolved neutral fill input color for the active state of the neutral-fill-input recipe. See {@link @microsoft/fast-components#neutralFillInputActive} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-input-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-input-active-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillInputActiveDelta) + public neutralFillInputActiveDelta?: number; + + /** + * The distance from the resolved neutral fill input color for the focus state of the neutral-fill-input recipe. See {@link @microsoft/fast-components#neutralFillInputFocus} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-input-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-input-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillInputFocusDelta) + public neutralFillInputFocusDelta?: number; + + /** + * The distance from the resolved neutral fill stealth color for the rest state of the neutral-fill-stealth recipe. See {@link @microsoft/fast-components#neutralFillStealthRest} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-stealth-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-stealth-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStealthRestDelta) + public neutralFillStealthRestDelta?: number; + + /** + * The distance from the resolved neutral fill stealth color for the hover state of the neutral-fill-stealth recipe. See {@link @microsoft/fast-components#neutralFillStealthHover} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-stealth-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-stealth-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStealthHoverDelta) + public neutralFillStealthHoverDelta?: number; + + /** + * The distance from the resolved neutral fill stealth color for the active state of the neutral-fill-stealth recipe. See {@link @microsoft/fast-components#neutralFillStealthActive} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-stealth-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-stealth-active-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStealthActiveDelta) + public neutralFillStealthActiveDelta?: number; + + /** + * The distance from the resolved neutral fill stealth color for the focus state of the neutral-fill-stealth recipe. See {@link @microsoft/fast-components#neutralFillStealthFocus} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-stealth-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-stealth-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStealthFocusDelta) + public neutralFillStealthFocusDelta?: number; + + /** + * The distance from the resolved neutral fill strong color for the hover state of the neutral-fill-strong recipe. See {@link @microsoft/fast-components#neutralFillStrongHover} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-strong-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-strong-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStrongHoverDelta) + public neutralFillStrongHoverDelta?: number; + + /** + * The distance from the resolved neutral fill strong color for the active state of the neutral-fill-strong recipe. See {@link @microsoft/fast-components#neutralFillStrongActive} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-strong-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-strong-active-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStrongActiveDelta) + public neutralFillStrongActiveDelta?: number; + + /** + * The distance from the resolved neutral fill strong color for the focus state of the neutral-fill-strong recipe. See {@link @microsoft/fast-components#neutralFillStrongFocus} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-strong-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-strong-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillStrongFocusDelta) + public neutralFillStrongFocusDelta?: number; + + /** + * The {@link https://www.w3.org/WAI/GL/wiki/Relative_luminance#:~:text=WCAG%20definition%20of%20relative%20luminance,and%201%20for%20lightest%20white|relative luminance} of the base layer of the application. + * + * @remarks + * When set to a number between 0 and 1, this values controls the output of {@link @microsoft/fast-components#neutralFillLayerRest}, {@link @microsoft/fast-components#neutralLayerCardContainer}, {@link @microsoft/fast-components#neutralLayerFloating}, {@link @microsoft/fast-components#neutralLayer1}, {@link @microsoft/fast-components#neutralLayer2}, {@link @microsoft/fast-components#neutralLayer3}, {@link @microsoft/fast-components#neutralLayer4}. + * + * HTML attribute: base-layer-luminance + * + * CSS custom property: N/A + */ + @attr({ + attribute: "base-layer-luminance", + converter: nullableNumberConverter, + }) + @designToken(baseLayerLuminance) + public baseLayerLuminance?: number; // 0...1 + + /** + * The distance from the background-color to resolve the card background. See {@link @microsoft/fast-components#neutralFillLayerRest} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-fill-layer-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-fill-layer-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralFillLayerRestDelta) + public neutralFillLayerRestDelta?: number; + + /** + * The distance from the resolved neutral divider color for the rest state of the neutral-foreground recipe. See {@link @microsoft/fast-components#neutralStrokeDividerRest} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-stroke-divider-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-stroke-divider-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralStrokeDividerRestDelta) + public neutralStrokeDividerRestDelta?: number; + + /** + * The distance from the resolved neutral stroke color for the rest state of the neutral-stroke recipe. See {@link @microsoft/fast-components#neutralStrokeRest} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-stroke-rest-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-stroke-rest-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralStrokeRestDelta) + public neutralStrokeRestDelta?: number; + + /** + * The distance from the resolved neutral stroke color for the hover state of the neutral-stroke recipe. See {@link @microsoft/fast-components#neutralStrokeHover} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-stroke-hover-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-stroke-hover-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralStrokeHoverDelta) + public neutralStrokeHoverDelta?: number; + + /** + * The distance from the resolved neutral stroke color for the active state of the neutral-stroke recipe. See {@link @microsoft/fast-components#neutralStrokeActive} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-stroke-active-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-stroke-active-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralStrokeActiveDelta) + public neutralStrokeActiveDelta?: number; + + /** + * The distance from the resolved neutral stroke color for the focus state of the neutral-stroke recipe. See {@link @microsoft/fast-components#neutralStrokeFocus} for usage in CSS. + * + * @remarks + * HTML attribute: neutral-stroke-focus-delta + * + * CSS custom property: N/A + */ + @attr({ + attribute: "neutral-stroke-focus-delta", + converter: nullableNumberConverter, + }) + @designToken(neutralStrokeFocusDelta) + public neutralStrokeFocusDelta?: number; +} + +/** + * Template for DesignSystemProvider. + * @public + */ +export const designSystemProviderTemplate = ( + context: ElementDefinitionContext, + definition: FoundationElementDefinition +) => html` + +`; + +/** + * Styles for DesignSystemProvider. + * @public + */ +export const designSystemProviderStyles = ( + context: ElementDefinitionContext, + definition: FoundationElementDefinition +) => css` + ${display("block")} +`; + +/** + * A function that returns a {@link DesignSystemProvider} registration for configuring the component with a DesignSystem. + * @public + * @remarks + * Generates HTML Element: `` + */ +export const fastDesignSystemProvider = DesignSystemProvider.compose({ + baseName: "design-system-provider", + template: designSystemProviderTemplate, + styles: designSystemProviderStyles, +}); diff --git a/packages/components/src/design-tokens.ts b/packages/components/src/design-tokens.ts index f2bbc2d9..91bc3550 100644 --- a/packages/components/src/design-tokens.ts +++ b/packages/components/src/design-tokens.ts @@ -1,381 +1,1185 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - accentFillActiveDelta, - accentFillFocusDelta, - accentFillHoverDelta, - accentForegroundActiveDelta, - accentForegroundFocusDelta, - accentForegroundHoverDelta, - accentForegroundRestDelta, - ColorRecipe, - disabledOpacity, - fillColor, - InteractiveColorRecipe, - InteractiveSwatchSet, - neutralFillActiveDelta, - neutralFillHoverDelta, - neutralFillRestDelta, - neutralPalette, - Palette, - PaletteRGB, - Swatch -} from '@microsoft/fast-components'; -import { DesignToken } from '@microsoft/fast-foundation'; +import { DesignToken } from "@microsoft/fast-foundation"; +import { Direction } from "@microsoft/fast-web-utilities"; +import { Palette, PaletteRGB } from "./color/palette.js"; +import { Swatch, SwatchRGB } from "./color/swatch.js"; +import { accentFill as accentFillAlgorithm } from "./color/recipes/accent-fill.js"; +import { accentForeground as accentForegroundAlgorithm } from "./color/recipes/accent-foreground.js"; +import { foregroundOnAccent as foregroundOnAccentAlgorithm } from "./color/recipes/foreground-on-accent.js"; +import { neutralFill as neutralFillAlgorithm } from "./color/recipes/neutral-fill.js"; +import { neutralFillInput as neutralFillInputAlgorithm } from "./color/recipes/neutral-fill-input.js"; +import { neutralFillLayer as neutralFillLayerAlgorithm } from "./color/recipes/neutral-fill-layer.js"; +import { neutralFillStealth as neutralFillStealthAlgorithm } from "./color/recipes/neutral-fill-stealth.js"; +import { neutralFillContrast as neutralFillContrastAlgorithm } from "./color/recipes/neutral-fill-contrast.js"; import { - ContrastTarget, - errorBase, - errorFillAlgorithm, - errorForegroundAlgorithm, - foregroundOnErrorAlgorithm -} from './color'; - -// Export design token from @microsoft/fast-components -// to encapsulate them. - -export { - accentColor, - accentFillActive, - accentFillActiveDelta, - accentFillFocus, - accentFillFocusDelta, - accentFillHover, - accentFillHoverDelta, - accentFillRecipe, - accentFillRest, - accentFillRestDelta, - accentForegroundActive, - accentForegroundActiveDelta, - accentForegroundFocus, - accentForegroundFocusDelta, - accentForegroundHover, - accentForegroundHoverDelta, - accentForegroundRecipe, - accentForegroundRest, - accentForegroundRestDelta, - accentPalette, - baseHeightMultiplier, - baseHorizontalSpacingMultiplier, - baseLayerLuminance, - bodyFont, - ColorRecipe, - controlCornerRadius, - density, - designUnit, - direction, - DirectionalStyleSheetBehavior, - disabledOpacity, - fillColor, - focusStrokeInner, - focusStrokeInnerRecipe, - focusStrokeOuter, - focusStrokeOuterRecipe, - focusStrokeWidth, - foregroundOnAccentActive, - foregroundOnAccentActiveLarge, - foregroundOnAccentFocus, - foregroundOnAccentFocusLarge, - foregroundOnAccentHover, - foregroundOnAccentHoverLarge, - foregroundOnAccentLargeRecipe, - foregroundOnAccentRecipe, - foregroundOnAccentRest, - foregroundOnAccentRestLarge, - InteractiveColorRecipe, - neutralColor, - neutralFillActive, - neutralFillActiveDelta, - neutralFillFocus, - neutralFillFocusDelta, - neutralFillHover, - neutralFillHoverDelta, - neutralFillInputActive, - neutralFillInputActiveDelta, - neutralFillInputFocus, - neutralFillInputFocusDelta, - neutralFillInputHover, - neutralFillInputHoverDelta, - neutralFillInputRecipe, - neutralFillInputRest, - neutralFillInputRestDelta, - neutralFillLayerRecipe, - neutralFillLayerRest, - neutralFillLayerRestDelta, - neutralFillRecipe, - neutralFillRest, - neutralFillRestDelta, - neutralFillStealthActive, - neutralFillStealthActiveDelta, - neutralFillStealthFocus, - neutralFillStealthFocusDelta, - neutralFillStealthHover, - neutralFillStealthHoverDelta, - neutralFillStealthRecipe, - neutralFillStealthRest, - neutralFillStealthRestDelta, - neutralFillStrongActive, - neutralFillStrongActiveDelta, - neutralFillStrongFocus, - neutralFillStrongFocusDelta, - neutralFillStrongHover, - neutralFillStrongHoverDelta, - neutralFillStrongRecipe, - neutralFillStrongRest, - neutralFillStrongRestDelta, - neutralForegroundHint, - neutralForegroundHintRecipe, - neutralForegroundRecipe, - neutralForegroundRest, - neutralLayer1, - neutralLayer1Recipe, - neutralLayer2, - neutralLayer2Recipe, - neutralLayer3, - neutralLayer3Recipe, - neutralLayer4, - neutralLayer4Recipe, - neutralLayerCardContainer, - neutralLayerCardContainerRecipe, - neutralLayerFloating, - neutralLayerFloatingRecipe, - neutralPalette, - neutralStrokeActive, - neutralStrokeActiveDelta, - neutralStrokeDividerRecipe, - neutralStrokeDividerRest, - neutralStrokeDividerRestDelta, - neutralStrokeFocus, - neutralStrokeFocusDelta, - neutralStrokeHover, - neutralStrokeHoverDelta, - neutralStrokeRecipe, - neutralStrokeRest, - neutralStrokeRestDelta, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight, - typeRampMinus1FontSize, - typeRampMinus1LineHeight, - typeRampMinus2FontSize, - typeRampMinus2LineHeight, - typeRampPlus1FontSize, - typeRampPlus1LineHeight, - typeRampPlus2FontSize, - typeRampPlus2LineHeight, - typeRampPlus3FontSize, - typeRampPlus3LineHeight, - typeRampPlus4FontSize, - typeRampPlus4LineHeight, - typeRampPlus5FontSize, - typeRampPlus5LineHeight, - typeRampPlus6FontSize, - typeRampPlus6LineHeight -} from '@microsoft/fast-components'; + focusStrokeInner as focusStrokeInnerAlgorithm, + focusStrokeOuter as focusStrokeOuterAlgorithm, +} from "./color/recipes/focus-stroke.js"; +import { neutralForeground as neutralForegroundAlgorithm } from "./color/recipes/neutral-foreground.js"; +import { neutralForegroundHint as neutralForegroundHintAlgorithm } from "./color/recipes/neutral-foreground-hint.js"; +import { neutralLayerCardContainer as neutralLayerCardContainerAlgorithm } from "./color/recipes/neutral-layer-card-container.js"; +import { neutralLayerFloating as neutralLayerFloatingAlgorithm } from "./color/recipes/neutral-layer-floating.js"; +import { neutralLayer1 as neutralLayer1Algorithm } from "./color/recipes/neutral-layer-1.js"; +import { neutralLayer2 as neutralLayer2Algorithm } from "./color/recipes/neutral-layer-2.js"; +import { neutralLayer3 as neutralLayer3Algorithm } from "./color/recipes/neutral-layer-3.js"; +import { neutralLayer4 as neutralLayer4Algorithm } from "./color/recipes/neutral-layer-4.js"; +import { neutralStroke as neutralStrokeAlgorithm } from "./color/recipes/neutral-stroke.js"; +import { neutralStrokeDivider as neutralStrokeDividerAlgorithm } from "./color/recipes/neutral-stroke-divider.js"; +import { StandardLuminance } from "./color/utilities/base-layer-luminance.js"; +import { accentBase, middleGrey } from "./color/utilities/color-constants.js"; +import { InteractiveSwatchSet } from "./color/recipe.js"; + +/** @public @deprecated Use ColorRecipe instead */ +export interface Recipe { + evaluate(element: HTMLElement, reference?: Swatch): T; +} + +/** @public */ +export interface ColorRecipe { + evaluate(element: HTMLElement, reference?: Swatch): Swatch; +} + +/** @public */ +export interface InteractiveColorRecipe { + evaluate(element: HTMLElement, reference?: Swatch): InteractiveSwatchSet; +} const { create } = DesignToken; -// Changing the default to increase contrast -disabledOpacity.withDefault(0.4); +function createNonCss(name: string): DesignToken { + return DesignToken.create({ name, cssCustomPropertyName: null }); +} -/* - * The error palette is built using the same color algorithm as the accent palette - * But by copying the algorithm from @microsoft/fast-components at commit 03d711f222bd816834a5e1d60256d3e083b27c27 - * as some helpers are not exported. - * The delta used are those of the accent palette. - */ +// General tokens -/** - * Error palette - */ -export const errorPalette = create({ - name: 'error-palette', - cssCustomPropertyName: null -}).withDefault(PaletteRGB.from(errorBase)); +/** @public */ +export const bodyFont = create("body-font").withDefault( + 'aktiv-grotesk, "Segoe UI", Arial, Helvetica, sans-serif' +); +/** @public */ +export const baseHeightMultiplier = create("base-height-multiplier").withDefault( + 10 +); +/** @public */ +export const baseHorizontalSpacingMultiplier = create( + "base-horizontal-spacing-multiplier" +).withDefault(3); +/** @public */ +export const baseLayerLuminance = create("base-layer-luminance").withDefault( + StandardLuminance.DarkMode +); +/** @public */ +export const controlCornerRadius = create("control-corner-radius").withDefault(4); +/** @public */ +export const density = create("density").withDefault(0); +/** @public */ +export const designUnit = create("design-unit").withDefault(4); +/** @public */ +export const direction = create("direction").withDefault(Direction.ltr); +/** @public */ +export const disabledOpacity = create("disabled-opacity").withDefault(0.4); +/** @public */ +export const strokeWidth = create("stroke-width").withDefault(1); +/** @public */ +export const focusStrokeWidth = create("focus-stroke-width").withDefault(2); + +// Typography values + +/** @public */ +export const typeRampBaseFontSize = create( + "type-ramp-base-font-size" +).withDefault("14px"); +/** @public */ +export const typeRampBaseLineHeight = create( + "type-ramp-base-line-height" +).withDefault("20px"); +/** @public */ +export const typeRampMinus1FontSize = create( + "type-ramp-minus-1-font-size" +).withDefault("12px"); +/** @public */ +export const typeRampMinus1LineHeight = create( + "type-ramp-minus-1-line-height" +).withDefault("16px"); +/** @public */ +export const typeRampMinus2FontSize = create( + "type-ramp-minus-2-font-size" +).withDefault("10px"); +/** @public */ +export const typeRampMinus2LineHeight = create( + "type-ramp-minus-2-line-height" +).withDefault("16px"); +/** @public */ +export const typeRampPlus1FontSize = create( + "type-ramp-plus-1-font-size" +).withDefault("16px"); +/** @public */ +export const typeRampPlus1LineHeight = create( + "type-ramp-plus-1-line-height" +).withDefault("24px"); +/** @public */ +export const typeRampPlus2FontSize = create( + "type-ramp-plus-2-font-size" +).withDefault("20px"); +/** @public */ +export const typeRampPlus2LineHeight = create( + "type-ramp-plus-2-line-height" +).withDefault("28px"); +/** @public */ +export const typeRampPlus3FontSize = create( + "type-ramp-plus-3-font-size" +).withDefault("28px"); +/** @public */ +export const typeRampPlus3LineHeight = create( + "type-ramp-plus-3-line-height" +).withDefault("36px"); +/** @public */ +export const typeRampPlus4FontSize = create( + "type-ramp-plus-4-font-size" +).withDefault("34px"); +/** @public */ +export const typeRampPlus4LineHeight = create( + "type-ramp-plus-4-line-height" +).withDefault("44px"); +/** @public */ +export const typeRampPlus5FontSize = create( + "type-ramp-plus-5-font-size" +).withDefault("46px"); +/** @public */ +export const typeRampPlus5LineHeight = create( + "type-ramp-plus-5-line-height" +).withDefault("56px"); +/** @public */ +export const typeRampPlus6FontSize = create( + "type-ramp-plus-6-font-size" +).withDefault("60px"); +/** @public */ +export const typeRampPlus6LineHeight = create( + "type-ramp-plus-6-line-height" +).withDefault("72px"); + +// Color recipe values + +/** @public */ +export const accentFillRestDelta = createNonCss( + "accent-fill-rest-delta" +).withDefault(0); +/** @public */ +export const accentFillHoverDelta = createNonCss( + "accent-fill-hover-delta" +).withDefault(4); +/** @public */ +export const accentFillActiveDelta = createNonCss( + "accent-fill-active-delta" +).withDefault(-5); +/** @public */ +export const accentFillFocusDelta = createNonCss( + "accent-fill-focus-delta" +).withDefault(0); + +/** @public */ +export const accentForegroundRestDelta = createNonCss( + "accent-foreground-rest-delta" +).withDefault(0); +/** @public */ +export const accentForegroundHoverDelta = createNonCss( + "accent-foreground-hover-delta" +).withDefault(6); +/** @public */ +export const accentForegroundActiveDelta = createNonCss( + "accent-foreground-active-delta" +).withDefault(-4); +/** @public */ +export const accentForegroundFocusDelta = createNonCss( + "accent-foreground-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillRestDelta = createNonCss( + "neutral-fill-rest-delta" +).withDefault(7); +/** @public */ +export const neutralFillHoverDelta = createNonCss( + "neutral-fill-hover-delta" +).withDefault(10); +/** @public */ +export const neutralFillActiveDelta = createNonCss( + "neutral-fill-active-delta" +).withDefault(5); +/** @public */ +export const neutralFillFocusDelta = createNonCss( + "neutral-fill-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillInputRestDelta = createNonCss( + "neutral-fill-input-rest-delta" +).withDefault(0); +/** @public */ +export const neutralFillInputHoverDelta = createNonCss( + "neutral-fill-input-hover-delta" +).withDefault(0); +/** @public */ +export const neutralFillInputActiveDelta = createNonCss( + "neutral-fill-input-active-delta" +).withDefault(0); +/** @public */ +export const neutralFillInputFocusDelta = createNonCss( + "neutral-fill-input-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillStealthRestDelta = createNonCss( + "neutral-fill-stealth-rest-delta" +).withDefault(0); +/** @public */ +export const neutralFillStealthHoverDelta = createNonCss( + "neutral-fill-stealth-hover-delta" +).withDefault(5); +/** @public */ +export const neutralFillStealthActiveDelta = createNonCss( + "neutral-fill-stealth-active-delta" +).withDefault(3); +/** @public */ +export const neutralFillStealthFocusDelta = createNonCss( + "neutral-fill-stealth-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillStrongRestDelta = createNonCss( + "neutral-fill-strong-rest-delta" +).withDefault(0); +/** @public */ +export const neutralFillStrongHoverDelta = createNonCss( + "neutral-fill-strong-hover-delta" +).withDefault(8); +/** @public */ +export const neutralFillStrongActiveDelta = createNonCss( + "neutral-fill-strong-active-delta" +).withDefault(-5); +/** @public */ +export const neutralFillStrongFocusDelta = createNonCss( + "neutral-fill-strong-focus-delta" +).withDefault(0); + +/** @public */ +export const neutralFillLayerRestDelta = createNonCss( + "neutral-fill-layer-rest-delta" +).withDefault(3); + +/** @public */ +export const neutralStrokeRestDelta = createNonCss( + "neutral-stroke-rest-delta" +).withDefault(25); +/** @public */ +export const neutralStrokeHoverDelta = createNonCss( + "neutral-stroke-hover-delta" +).withDefault(40); +/** @public */ +export const neutralStrokeActiveDelta = createNonCss( + "neutral-stroke-active-delta" +).withDefault(16); +/** @public */ +export const neutralStrokeFocusDelta = createNonCss( + "neutral-stroke-focus-delta" +).withDefault(25); + +/** @public */ +export const neutralStrokeDividerRestDelta = createNonCss( + "neutral-stroke-divider-rest-delta" +).withDefault(8); + +// Color recipes + +/** @public */ +export const neutralColor = create("neutral-color").withDefault(middleGrey); + +/** @public */ +export const neutralPalette = createNonCss( + "neutral-palette" +).withDefault((element: HTMLElement) => + PaletteRGB.from(neutralColor.getValueFor(element) as SwatchRGB) +); + +/** @public */ +export const accentColor = create("accent-color").withDefault(accentBase); + +/** @public */ +export const accentPalette = createNonCss( + "accent-palette" +).withDefault((element: HTMLElement) => + PaletteRGB.from(accentColor.getValueFor(element) as SwatchRGB) +); + +// Neutral Layer Card Container +/** @public */ +export const neutralLayerCardContainerRecipe = createNonCss( + "neutral-layer-card-container-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralLayerCardContainerAlgorithm( + neutralPalette.getValueFor(element), + baseLayerLuminance.getValueFor(element), + neutralFillLayerRestDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralLayerCardContainer = create( + "neutral-layer-card-container" +).withDefault((element: HTMLElement) => + neutralLayerCardContainerRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Layer Floating +/** @public */ +export const neutralLayerFloatingRecipe = createNonCss( + "neutral-layer-floating-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralLayerFloatingAlgorithm( + neutralPalette.getValueFor(element), + baseLayerLuminance.getValueFor(element), + neutralFillLayerRestDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralLayerFloating = create( + "neutral-layer-floating" +).withDefault((element: HTMLElement) => + neutralLayerFloatingRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Layer 1 +/** @public */ +export const neutralLayer1Recipe = createNonCss( + "neutral-layer-1-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralLayer1Algorithm( + neutralPalette.getValueFor(element), + baseLayerLuminance.getValueFor(element) + ), +}); + +/** @public */ +export const neutralLayer1 = create( + "neutral-layer-1" +).withDefault((element: HTMLElement) => + neutralLayer1Recipe.getValueFor(element).evaluate(element) +); + +// Neutral Layer 2 +/** @public */ +export const neutralLayer2Recipe = createNonCss( + "neutral-layer-2-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralLayer2Algorithm( + neutralPalette.getValueFor(element), + baseLayerLuminance.getValueFor(element), + neutralFillLayerRestDelta.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralLayer2 = create( + "neutral-layer-2" +).withDefault((element: HTMLElement) => + neutralLayer2Recipe.getValueFor(element).evaluate(element) +); + +// Neutral Layer 3 +/** @public */ +export const neutralLayer3Recipe = createNonCss( + "neutral-layer-3-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralLayer3Algorithm( + neutralPalette.getValueFor(element), + baseLayerLuminance.getValueFor(element), + neutralFillLayerRestDelta.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralLayer3 = create( + "neutral-layer-3" +).withDefault((element: HTMLElement) => + neutralLayer3Recipe.getValueFor(element).evaluate(element) +); + +// Neutral Layer 4 +/** @public */ +export const neutralLayer4Recipe = createNonCss( + "neutral-layer-4-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralLayer4Algorithm( + neutralPalette.getValueFor(element), + baseLayerLuminance.getValueFor(element), + neutralFillLayerRestDelta.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element) + ), +}); -// Error Fill /** @public */ -export const errorFillRecipe = create({ - name: 'error-fill-recipe', - cssCustomPropertyName: null +export const neutralLayer4 = create( + "neutral-layer-4" +).withDefault((element: HTMLElement) => + neutralLayer4Recipe.getValueFor(element).evaluate(element) +); + +/** @public */ +export const fillColor = create("fill-color").withDefault(element => + neutralLayer1.getValueFor(element) +); + +enum ContrastTarget { + normal = 4.5, + large = 7, +} + +// Accent Fill +/** @public */ +export const accentFillRecipe = create({ + name: "accent-fill-recipe", + cssCustomPropertyName: null, }).withDefault({ - evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => - errorFillAlgorithm( - errorPalette.getValueFor(element), - neutralPalette.getValueFor(element), - reference || fillColor.getValueFor(element), - accentFillHoverDelta.getValueFor(element), - accentFillActiveDelta.getValueFor(element), - accentFillFocusDelta.getValueFor(element), - neutralFillRestDelta.getValueFor(element), - neutralFillHoverDelta.getValueFor(element), - neutralFillActiveDelta.getValueFor(element) - ) + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + accentFillAlgorithm( + accentPalette.getValueFor(element), + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + accentFillHoverDelta.getValueFor(element), + accentFillActiveDelta.getValueFor(element), + accentFillFocusDelta.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element) + ), }); /** @public */ -export const errorFillRest = create('error-fill-rest').withDefault( - (element: HTMLElement) => { - return errorFillRecipe.getValueFor(element).evaluate(element).rest; - } +export const accentFillRest = create("accent-fill-rest").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).rest; + } ); /** @public */ -export const errorFillHover = create('error-fill-hover').withDefault( - (element: HTMLElement) => { - return errorFillRecipe.getValueFor(element).evaluate(element).hover; - } +export const accentFillHover = create("accent-fill-hover").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).hover; + } ); /** @public */ -export const errorFillActive = create('error-fill-active').withDefault( - (element: HTMLElement) => { - return errorFillRecipe.getValueFor(element).evaluate(element).active; - } +export const accentFillActive = create("accent-fill-active").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).active; + } ); /** @public */ -export const errorFillFocus = create('error-fill-focus').withDefault( - (element: HTMLElement) => { - return errorFillRecipe.getValueFor(element).evaluate(element).focus; - } +export const accentFillFocus = create("accent-fill-focus").withDefault( + (element: HTMLElement) => { + return accentFillRecipe.getValueFor(element).evaluate(element).focus; + } ); -// Foreground On Error -const foregroundOnErrorByContrast = - (contrast: number) => (element: HTMLElement, reference?: Swatch) => { - return foregroundOnErrorAlgorithm( - reference || errorFillRest.getValueFor(element), - contrast +// Foreground On Accent +const foregroundOnAccentByContrast = (contrast: number) => ( + element: HTMLElement, + reference?: Swatch +) => { + return foregroundOnAccentAlgorithm( + reference || accentFillRest.getValueFor(element), + contrast ); - }; +}; /** @public */ -export const foregroundOnErrorRecipe = create({ - name: 'foreground-on-error-recipe', - cssCustomPropertyName: null -}).withDefault({ - evaluate: (element: HTMLElement, reference?: Swatch): Swatch => - foregroundOnErrorByContrast(ContrastTarget.normal)(element, reference) +export const foregroundOnAccentRecipe = createNonCss( + "foreground-on-accent-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + foregroundOnAccentByContrast(ContrastTarget.normal)(element, reference), }); /** @public */ -export const foregroundOnErrorRest = create( - 'foreground-on-error-rest' +export const foregroundOnAccentRest = create( + "foreground-on-accent-rest" ).withDefault((element: HTMLElement) => - foregroundOnErrorRecipe - .getValueFor(element) - .evaluate(element, errorFillRest.getValueFor(element)) + foregroundOnAccentRecipe + .getValueFor(element) + .evaluate(element, accentFillRest.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorHover = create( - 'foreground-on-error-hover' +export const foregroundOnAccentHover = create( + "foreground-on-accent-hover" ).withDefault((element: HTMLElement) => - foregroundOnErrorRecipe - .getValueFor(element) - .evaluate(element, errorFillHover.getValueFor(element)) + foregroundOnAccentRecipe + .getValueFor(element) + .evaluate(element, accentFillHover.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorActive = create( - 'foreground-on-error-active' +export const foregroundOnAccentActive = create( + "foreground-on-accent-active" ).withDefault((element: HTMLElement) => - foregroundOnErrorRecipe - .getValueFor(element) - .evaluate(element, errorFillActive.getValueFor(element)) + foregroundOnAccentRecipe + .getValueFor(element) + .evaluate(element, accentFillActive.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorFocus = create( - 'foreground-on-error-focus' +export const foregroundOnAccentFocus = create( + "foreground-on-accent-focus" ).withDefault((element: HTMLElement) => - foregroundOnErrorRecipe - .getValueFor(element) - .evaluate(element, errorFillFocus.getValueFor(element)) + foregroundOnAccentRecipe + .getValueFor(element) + .evaluate(element, accentFillFocus.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorLargeRecipe = create({ - name: 'foreground-on-error-large-recipe', - cssCustomPropertyName: null -}).withDefault({ - evaluate: (element: HTMLElement, reference?: Swatch): Swatch => - foregroundOnErrorByContrast(ContrastTarget.large)(element, reference) +export const foregroundOnAccentLargeRecipe = createNonCss( + "foreground-on-accent-large-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + foregroundOnAccentByContrast(ContrastTarget.large)(element, reference), }); /** @public */ -export const foregroundOnErrorRestLarge = create( - 'foreground-on-error-rest-large' +export const foregroundOnAccentRestLarge = create( + "foreground-on-accent-rest-large" ).withDefault((element: HTMLElement) => - foregroundOnErrorLargeRecipe - .getValueFor(element) - .evaluate(element, errorFillRest.getValueFor(element)) + foregroundOnAccentLargeRecipe + .getValueFor(element) + .evaluate(element, accentFillRest.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorHoverLarge = create( - 'foreground-on-error-hover-large' +export const foregroundOnAccentHoverLarge = create( + "foreground-on-accent-hover-large" ).withDefault((element: HTMLElement) => - foregroundOnErrorLargeRecipe - .getValueFor(element) - .evaluate(element, errorFillHover.getValueFor(element)) + foregroundOnAccentLargeRecipe + .getValueFor(element) + .evaluate(element, accentFillHover.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorActiveLarge = create( - 'foreground-on-error-active-large' +export const foregroundOnAccentActiveLarge = create( + "foreground-on-accent-active-large" ).withDefault((element: HTMLElement) => - foregroundOnErrorLargeRecipe - .getValueFor(element) - .evaluate(element, errorFillActive.getValueFor(element)) + foregroundOnAccentLargeRecipe + .getValueFor(element) + .evaluate(element, accentFillActive.getValueFor(element)) ); /** @public */ -export const foregroundOnErrorFocusLarge = create( - 'foreground-on-error-focus-large' +export const foregroundOnAccentFocusLarge = create( + "foreground-on-accent-focus-large" ).withDefault((element: HTMLElement) => - foregroundOnErrorLargeRecipe - .getValueFor(element) - .evaluate(element, errorFillFocus.getValueFor(element)) -); - -// Error Foreground -const errorForegroundByContrast = - (contrast: number) => (element: HTMLElement, reference?: Swatch) => - errorForegroundAlgorithm( - errorPalette.getValueFor(element), - reference || fillColor.getValueFor(element), - contrast, - accentForegroundRestDelta.getValueFor(element), - accentForegroundHoverDelta.getValueFor(element), - accentForegroundActiveDelta.getValueFor(element), - accentForegroundFocusDelta.getValueFor(element) + foregroundOnAccentLargeRecipe + .getValueFor(element) + .evaluate(element, accentFillFocus.getValueFor(element)) +); + +// Accent Foreground +const accentForegroundByContrast = (contrast: number) => ( + element: HTMLElement, + reference?: Swatch +) => + accentForegroundAlgorithm( + accentPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + contrast, + accentForegroundRestDelta.getValueFor(element), + accentForegroundHoverDelta.getValueFor(element), + accentForegroundActiveDelta.getValueFor(element), + accentForegroundFocusDelta.getValueFor(element) ); /** @public */ -export const errorForegroundRecipe = create({ - name: 'error-foreground-recipe', - cssCustomPropertyName: null +export const accentForegroundRecipe = create({ + name: "accent-foreground-recipe", + cssCustomPropertyName: null, }).withDefault({ - evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => - errorForegroundByContrast(ContrastTarget.normal)(element, reference) + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + accentForegroundByContrast(ContrastTarget.normal)(element, reference), }); /** @public */ -export const errorForegroundRest = create( - 'error-foreground-rest' +export const accentForegroundRest = create("accent-foreground-rest").withDefault( + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).rest +); +/** @public */ +export const accentForegroundHover = create( + "accent-foreground-hover" ).withDefault( - (element: HTMLElement) => - errorForegroundRecipe.getValueFor(element).evaluate(element).rest + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).hover ); /** @public */ -export const errorForegroundHover = create( - 'error-foreground-hover' +export const accentForegroundActive = create( + "accent-foreground-active" ).withDefault( - (element: HTMLElement) => - errorForegroundRecipe.getValueFor(element).evaluate(element).hover + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).active +); +/** @public */ +export const accentForegroundFocus = create( + "accent-foreground-focus" +).withDefault( + (element: HTMLElement) => + accentForegroundRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill +/** @public */ +export const neutralFillRecipe = create({ + name: "neutral-fill-recipe", + cssCustomPropertyName: null, +}).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + neutralFillAlgorithm( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element), + neutralFillFocusDelta.getValueFor(element) + ), +}); +/** @public */ +export const neutralFillRest = create("neutral-fill-rest").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).rest +); +/** @public */ +export const neutralFillHover = create("neutral-fill-hover").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).hover +); +/** @public */ +export const neutralFillActive = create("neutral-fill-active").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).active +); +/** @public */ +export const neutralFillFocus = create("neutral-fill-focus").withDefault( + (element: HTMLElement) => + neutralFillRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Input +/** @public */ +export const neutralFillInputRecipe = create({ + name: "neutral-fill-input-recipe", + cssCustomPropertyName: null, +}).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + neutralFillInputAlgorithm( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillInputRestDelta.getValueFor(element), + neutralFillInputHoverDelta.getValueFor(element), + neutralFillInputActiveDelta.getValueFor(element), + neutralFillInputFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillInputRest = create("neutral-fill-input-rest").withDefault( + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).rest ); /** @public */ -export const errorForegroundActive = create( - 'error-foreground-active' +export const neutralFillInputHover = create( + "neutral-fill-input-hover" ).withDefault( - (element: HTMLElement) => - errorForegroundRecipe.getValueFor(element).evaluate(element).active + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).hover ); /** @public */ -export const errorForegroundFocus = create( - 'error-foreground-focus' +export const neutralFillInputActive = create( + "neutral-fill-input-active" ).withDefault( - (element: HTMLElement) => - errorForegroundRecipe.getValueFor(element).evaluate(element).focus + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).active ); +/** @public */ +export const neutralFillInputFocus = create( + "neutral-fill-input-focus" +).withDefault( + (element: HTMLElement) => + neutralFillInputRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Stealth +/** @public */ +export const neutralFillStealthRecipe = create({ + name: "neutral-fill-stealth-recipe", + cssCustomPropertyName: null, +}).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + neutralFillStealthAlgorithm( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillStealthRestDelta.getValueFor(element), + neutralFillStealthHoverDelta.getValueFor(element), + neutralFillStealthActiveDelta.getValueFor(element), + neutralFillStealthFocusDelta.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element), + neutralFillFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillStealthRest = create( + "neutral-fill-stealth-rest" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).rest +); +/** @public */ +export const neutralFillStealthHover = create( + "neutral-fill-stealth-hover" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).hover +); +/** @public */ +export const neutralFillStealthActive = create( + "neutral-fill-stealth-active" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).active +); +/** @public */ +export const neutralFillStealthFocus = create( + "neutral-fill-stealth-focus" +).withDefault( + (element: HTMLElement) => + neutralFillStealthRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Strong +/** @public */ +export const neutralFillStrongRecipe = create({ + name: "neutral-fill-strong-recipe", + cssCustomPropertyName: null, +}).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + neutralFillContrastAlgorithm( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillStrongRestDelta.getValueFor(element), + neutralFillStrongHoverDelta.getValueFor(element), + neutralFillStrongActiveDelta.getValueFor(element), + neutralFillStrongFocusDelta.getValueFor(element) + ), +}); + +/** @public */ +export const neutralFillStrongRest = create( + "neutral-fill-strong-rest" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).rest +); +/** @public */ +export const neutralFillStrongHover = create( + "neutral-fill-strong-hover" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).hover +); +/** @public */ +export const neutralFillStrongActive = create( + "neutral-fill-strong-active" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).active +); +/** @public */ +export const neutralFillStrongFocus = create( + "neutral-fill-strong-focus" +).withDefault( + (element: HTMLElement) => + neutralFillStrongRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Fill Layer +/** @public */ +export const neutralFillLayerRecipe = createNonCss( + "neutral-fill-layer-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + neutralFillLayerAlgorithm( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralFillLayerRestDelta.getValueFor(element) + ), +}); +/** @public */ +export const neutralFillLayerRest = create( + "neutral-fill-layer-rest" +).withDefault((element: HTMLElement) => + neutralFillLayerRecipe.getValueFor(element).evaluate(element) +); + +// Focus Stroke Outer +/** @public */ +export const focusStrokeOuterRecipe = createNonCss( + "focus-stroke-outer-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + focusStrokeOuterAlgorithm( + neutralPalette.getValueFor(element), + fillColor.getValueFor(element) + ), +}); + +/** @public */ +export const focusStrokeOuter = create( + "focus-stroke-outer" +).withDefault((element: HTMLElement) => + focusStrokeOuterRecipe.getValueFor(element).evaluate(element) +); + +// Focus Stroke Inner +/** @public */ +export const focusStrokeInnerRecipe = createNonCss( + "focus-stroke-inner-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + focusStrokeInnerAlgorithm( + accentPalette.getValueFor(element), + fillColor.getValueFor(element), + focusStrokeOuter.getValueFor(element) + ), +}); + +/** @public */ +export const focusStrokeInner = create( + "focus-stroke-inner" +).withDefault((element: HTMLElement) => + focusStrokeInnerRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Foreground Hint +/** @public */ +export const neutralForegroundHintRecipe = createNonCss( + "neutral-foreground-hint-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralForegroundHintAlgorithm( + neutralPalette.getValueFor(element), + fillColor.getValueFor(element) + ), +}); + +/** @public */ +export const neutralForegroundHint = create( + "neutral-foreground-hint" +).withDefault((element: HTMLElement) => + neutralForegroundHintRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Foreground +/** @public */ +export const neutralForegroundRecipe = createNonCss( + "neutral-foreground-recipe" +).withDefault({ + evaluate: (element: HTMLElement): Swatch => + neutralForegroundAlgorithm( + neutralPalette.getValueFor(element), + fillColor.getValueFor(element) + ), +}); + +/** @public */ +export const neutralForegroundRest = create( + "neutral-foreground-rest" +).withDefault((element: HTMLElement) => + neutralForegroundRecipe.getValueFor(element).evaluate(element) +); + +// Neutral Stroke +/** @public */ +export const neutralStrokeRecipe = create({ + name: "neutral-stroke-recipe", + cssCustomPropertyName: null, +}).withDefault({ + evaluate: (element: HTMLElement): InteractiveSwatchSet => { + return neutralStrokeAlgorithm( + neutralPalette.getValueFor(element), + fillColor.getValueFor(element), + neutralStrokeRestDelta.getValueFor(element), + neutralStrokeHoverDelta.getValueFor(element), + neutralStrokeActiveDelta.getValueFor(element), + neutralStrokeFocusDelta.getValueFor(element) + ); + }, +}); + +/** @public */ +export const neutralStrokeRest = create("neutral-stroke-rest").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).rest +); +/** @public */ +export const neutralStrokeHover = create("neutral-stroke-hover").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).hover +); +/** @public */ +export const neutralStrokeActive = create("neutral-stroke-active").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).active +); +/** @public */ +export const neutralStrokeFocus = create("neutral-stroke-focus").withDefault( + (element: HTMLElement) => + neutralStrokeRecipe.getValueFor(element).evaluate(element).focus +); + +// Neutral Stroke Divider +/** @public */ +export const neutralStrokeDividerRecipe = createNonCss( + "neutral-stroke-divider-recipe" +).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + neutralStrokeDividerAlgorithm( + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + neutralStrokeDividerRestDelta.getValueFor(element) + ), +}); +/** @public */ +export const neutralStrokeDividerRest = create( + "neutral-stroke-divider-rest" +).withDefault(element => + neutralStrokeDividerRecipe.getValueFor(element).evaluate(element) +); + +/** + * The control height formula expressed as a design token. + * This token does not provide a CSS custom property. + * + * @public + */ +export const heightNumberAsToken = DesignToken.create({ + name: "height-number", + cssCustomPropertyName: null, +}).withDefault( + target => + (baseHeightMultiplier.getValueFor(target) + density.getValueFor(target)) * + designUnit.getValueFor(target) +); + +/* + * The error palette is built using the same color algorithm as the accent palette + * But by copying the algorithm from @microsoft/fast-components at commit 03d711f222bd816834a5e1d60256d3e083b27c27 + * as some helpers are not exported. + * The delta used are those of the accent palette. + */ + +/** + * Error palette + */ +export const errorPalette = create({ + name: 'error-palette', + cssCustomPropertyName: null + }).withDefault(PaletteRGB.from(errorBase)); + + // Error Fill + /** @public */ + export const errorFillRecipe = create({ + name: 'error-fill-recipe', + cssCustomPropertyName: null + }).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + errorFillAlgorithm( + errorPalette.getValueFor(element), + neutralPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + accentFillHoverDelta.getValueFor(element), + accentFillActiveDelta.getValueFor(element), + accentFillFocusDelta.getValueFor(element), + neutralFillRestDelta.getValueFor(element), + neutralFillHoverDelta.getValueFor(element), + neutralFillActiveDelta.getValueFor(element) + ) + }); + + /** @public */ + export const errorFillRest = create('error-fill-rest').withDefault( + (element: HTMLElement) => { + return errorFillRecipe.getValueFor(element).evaluate(element).rest; + } + ); + /** @public */ + export const errorFillHover = create('error-fill-hover').withDefault( + (element: HTMLElement) => { + return errorFillRecipe.getValueFor(element).evaluate(element).hover; + } + ); + /** @public */ + export const errorFillActive = create('error-fill-active').withDefault( + (element: HTMLElement) => { + return errorFillRecipe.getValueFor(element).evaluate(element).active; + } + ); + /** @public */ + export const errorFillFocus = create('error-fill-focus').withDefault( + (element: HTMLElement) => { + return errorFillRecipe.getValueFor(element).evaluate(element).focus; + } + ); + + // Foreground On Error + const foregroundOnErrorByContrast = + (contrast: number) => (element: HTMLElement, reference?: Swatch) => { + return foregroundOnErrorAlgorithm( + reference || errorFillRest.getValueFor(element), + contrast + ); + }; + + /** @public */ + export const foregroundOnErrorRecipe = create({ + name: 'foreground-on-error-recipe', + cssCustomPropertyName: null + }).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + foregroundOnErrorByContrast(ContrastTarget.normal)(element, reference) + }); + /** @public */ + export const foregroundOnErrorRest = create( + 'foreground-on-error-rest' + ).withDefault((element: HTMLElement) => + foregroundOnErrorRecipe + .getValueFor(element) + .evaluate(element, errorFillRest.getValueFor(element)) + ); + /** @public */ + export const foregroundOnErrorHover = create( + 'foreground-on-error-hover' + ).withDefault((element: HTMLElement) => + foregroundOnErrorRecipe + .getValueFor(element) + .evaluate(element, errorFillHover.getValueFor(element)) + ); + /** @public */ + export const foregroundOnErrorActive = create( + 'foreground-on-error-active' + ).withDefault((element: HTMLElement) => + foregroundOnErrorRecipe + .getValueFor(element) + .evaluate(element, errorFillActive.getValueFor(element)) + ); + /** @public */ + export const foregroundOnErrorFocus = create( + 'foreground-on-error-focus' + ).withDefault((element: HTMLElement) => + foregroundOnErrorRecipe + .getValueFor(element) + .evaluate(element, errorFillFocus.getValueFor(element)) + ); + + /** @public */ + export const foregroundOnErrorLargeRecipe = create({ + name: 'foreground-on-error-large-recipe', + cssCustomPropertyName: null + }).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): Swatch => + foregroundOnErrorByContrast(ContrastTarget.large)(element, reference) + }); + /** @public */ + export const foregroundOnErrorRestLarge = create( + 'foreground-on-error-rest-large' + ).withDefault((element: HTMLElement) => + foregroundOnErrorLargeRecipe + .getValueFor(element) + .evaluate(element, errorFillRest.getValueFor(element)) + ); + /** @public */ + export const foregroundOnErrorHoverLarge = create( + 'foreground-on-error-hover-large' + ).withDefault((element: HTMLElement) => + foregroundOnErrorLargeRecipe + .getValueFor(element) + .evaluate(element, errorFillHover.getValueFor(element)) + ); + /** @public */ + export const foregroundOnErrorActiveLarge = create( + 'foreground-on-error-active-large' + ).withDefault((element: HTMLElement) => + foregroundOnErrorLargeRecipe + .getValueFor(element) + .evaluate(element, errorFillActive.getValueFor(element)) + ); + /** @public */ + export const foregroundOnErrorFocusLarge = create( + 'foreground-on-error-focus-large' + ).withDefault((element: HTMLElement) => + foregroundOnErrorLargeRecipe + .getValueFor(element) + .evaluate(element, errorFillFocus.getValueFor(element)) + ); + + // Error Foreground + const errorForegroundByContrast = + (contrast: number) => (element: HTMLElement, reference?: Swatch) => + errorForegroundAlgorithm( + errorPalette.getValueFor(element), + reference || fillColor.getValueFor(element), + contrast, + accentForegroundRestDelta.getValueFor(element), + accentForegroundHoverDelta.getValueFor(element), + accentForegroundActiveDelta.getValueFor(element), + accentForegroundFocusDelta.getValueFor(element) + ); + + /** @public */ + export const errorForegroundRecipe = create({ + name: 'error-foreground-recipe', + cssCustomPropertyName: null + }).withDefault({ + evaluate: (element: HTMLElement, reference?: Swatch): InteractiveSwatchSet => + errorForegroundByContrast(ContrastTarget.normal)(element, reference) + }); + + /** @public */ + export const errorForegroundRest = create( + 'error-foreground-rest' + ).withDefault( + (element: HTMLElement) => + errorForegroundRecipe.getValueFor(element).evaluate(element).rest + ); + /** @public */ + export const errorForegroundHover = create( + 'error-foreground-hover' + ).withDefault( + (element: HTMLElement) => + errorForegroundRecipe.getValueFor(element).evaluate(element).hover + ); + /** @public */ + export const errorForegroundActive = create( + 'error-foreground-active' + ).withDefault( + (element: HTMLElement) => + errorForegroundRecipe.getValueFor(element).evaluate(element).active + ); + /** @public */ + export const errorForegroundFocus = create( + 'error-foreground-focus' + ).withDefault( + (element: HTMLElement) => + errorForegroundRecipe.getValueFor(element).evaluate(element).focus + ); + \ No newline at end of file diff --git a/packages/components/src/dialog/dialog.pw.spec.ts b/packages/components/src/dialog/dialog.pw.spec.ts new file mode 100644 index 00000000..a62c0f11 --- /dev/null +++ b/packages/components/src/dialog/dialog.pw.spec.ts @@ -0,0 +1,98 @@ +import type { Dialog as FASTDialogType } from "@microsoft/fast-foundation"; +import chai from "chai"; + +const { expect } = chai; + +type FASTDialog = HTMLElement & FASTDialogType; + +describe("FASTDialog", function () { + beforeEach(async function () { + if (!this.page && !this.browser) { + this.skip(); + } + + this.documentHandle = await this.page.evaluateHandle(() => document); + + this.setupHandle = await this.page.evaluateHandle( + (document) => { + const element = document.createElement("fast-dialog") as FASTDialog; + element.id = "testelement"; + + const button1 = document.createElement("button"); + button1.id = "button1"; + element.appendChild(button1); + + const button2 = document.createElement("button"); + button2.id = "button2"; + element.appendChild(button2); + + document.body.appendChild(element); + }, + this.documentHandle + ); + }); + + afterEach(async function () { + if (this.setupHandle) { + await this.setupHandle.dispose(); + } + }); + + // FASTDialog should render on the page + it("should render on the page", async function () { + const element = await this.page.$("fast-dialog"); + + expect(element).to.exist; + }); + + // FASTDialog should focus on the first element + it("should focus on first element", async function () { + const element = await this.page.$("fast-dialog"); + + expect( + await this.page.evaluate( + () => document.activeElement?.id + ) + ).to.equal("button1"); + }); + + // FASTDialog should trap focus + it("should trap focus", async function () { + const element = await this.page.$("fast-dialog"); + + expect( + await this.page.evaluate( + () => document.activeElement?.id + ) + ).to.equal("button1"); + + await element?.press("Tab"); + expect( + await this.page.evaluate( + () => document.activeElement?.id + ) + ).to.equal("button2"); + + await element?.press("Tab"); + expect( + await this.page.evaluate( + () => document.activeElement?.id + ) + ).to.equal("button1"); + + await element?.press("Shift+Tab"); + expect( + await this.page.evaluate( + () => document.activeElement?.id + ) + ).to.equal("button2"); + + await element?.press("Shift+Tab"); + expect( + await this.page.evaluate( + () => document.activeElement?.id + ) + ).to.equal("button1"); + + }); +}); diff --git a/packages/components/src/dialog/dialog.stories.ts b/packages/components/src/dialog/dialog.stories.ts index 2062757c..6559123d 100644 --- a/packages/components/src/dialog/dialog.stories.ts +++ b/packages/components/src/dialog/dialog.stories.ts @@ -1,54 +1,27 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import { STORY_RENDERED } from "@storybook/core-events"; +import addons from "@storybook/addons"; +import DialogTemplate from "./fixtures/dialog.html"; +import "./index.js"; -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("dialog")) { + const button1 = document.getElementById("button1"); + const dialog1 = document.getElementById("dialog1"); -export default { - title: 'Components/Dialog', - argTypes: { - trapFocus: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - }, - decorators: [ - story => ` - -
- ${story()} -
` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); + } +}); - return ` -
-

Dialog heading

- CancelOk -
-
`; +export default { + title: "Dialog", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - trapFocus: true -}; +export const Dialog = () => DialogTemplate; diff --git a/packages/components/src/dialog/dialog.styles.ts b/packages/components/src/dialog/dialog.styles.ts new file mode 100644 index 00000000..badecfe6 --- /dev/null +++ b/packages/components/src/dialog/dialog.styles.ts @@ -0,0 +1,57 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { controlCornerRadius, fillColor, strokeWidth } from "../design-tokens.js"; +import { elevation } from "../styles/elevation.js"; + +/** + * Styles for Dialog + * @public + */ +export const dialogStyles: FoundationElementTemplate = ( + context, + definition +) => css` + :host([hidden]) { + display: none; + } + + :host { + --elevation: 14; + --dialog-height: 480px; + --dialog-width: 640px; + display: block; + } + + .overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.3); + touch-action: none; + } + + .positioning-region { + display: flex; + justify-content: center; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + overflow: auto; + } + + .control { + ${elevation} + margin-top: auto; + margin-bottom: auto; + width: var(--dialog-width); + height: var(--dialog-height); + background-color: ${fillColor}; + z-index: 1; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid transparent; + } +`; diff --git a/packages/components/src/dialog/index.ts b/packages/components/src/dialog/index.ts index ba5c7c43..e50dc928 100644 --- a/packages/components/src/dialog/index.ts +++ b/packages/components/src/dialog/index.ts @@ -1,8 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { Dialog, dialogTemplate as template } from '@microsoft/fast-foundation'; -import { dialogStyles as styles } from '@microsoft/fast-components'; +import { Dialog, dialogTemplate as template } from "@microsoft/fast-foundation"; +import { dialogStyles as styles } from "./dialog.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Dialog} registration for configuring the component with a DesignSystem. @@ -11,13 +8,12 @@ import { dialogStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * HTML Element: `` - * + * Generates HTML Element: `` */ -export const jpDialog = Dialog.compose({ - baseName: 'dialog', - template, - styles +export const fastDialog = Dialog.compose({ + baseName: "dialog", + template, + styles, }); /** diff --git a/packages/components/src/disclosure/disclosure.stories.ts b/packages/components/src/disclosure/disclosure.stories.ts new file mode 100644 index 00000000..8e028b4c --- /dev/null +++ b/packages/components/src/disclosure/disclosure.stories.ts @@ -0,0 +1,8 @@ +import DisclosureTemplate from "./fixtures/disclosure.html"; +import "./index.js"; + +export default { + title: "Disclosure", +}; + +export const Disclosure = () => DisclosureTemplate; diff --git a/packages/components/src/disclosure/disclosure.styles.ts b/packages/components/src/disclosure/disclosure.styles.ts new file mode 100644 index 00000000..80d1f2b1 --- /dev/null +++ b/packages/components/src/disclosure/disclosure.styles.ts @@ -0,0 +1,91 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { + accentFillActive, + accentFillHover, + accentFillRest, + accentForegroundActive, + accentForegroundHover, + accentForegroundRest, + bodyFont, + controlCornerRadius, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + strokeWidth, + typeRampBaseFontSize, +} from "../design-tokens.js"; + +/** + * Styles for Disclosure + * @public + */ +export const disclosureStyles: FoundationElementTemplate = ( + context, + definition +) => css` + .disclosure { + transition: height 0.35s; + } + + .disclosure .invoker::-webkit-details-marker { + display: none; + } + + .disclosure .invoker { + list-style-type: none; + } + + :host([appearance="accent"]) .invoker { + background: ${accentFillRest}; + color: ${foregroundOnAccentRest}; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + border-radius: calc(${controlCornerRadius} * 1px); + outline: none; + cursor: pointer; + margin: 16px 0; + padding: 12px; + max-width: max-content; + } + + :host([appearance="accent"]) .invoker:active { + background: ${accentFillActive}; + color: ${foregroundOnAccentActive}; + } + + :host([appearance="accent"]) .invoker:hover { + background: ${accentFillHover}; + color: ${foregroundOnAccentHover}; + } + + :host([appearance="lightweight"]) .invoker { + background: transparent; + color: ${accentForegroundRest}; + border-bottom: calc(${strokeWidth} * 1px) solid ${accentForegroundRest}; + cursor: pointer; + width: max-content; + margin: 16px 0; + } + + :host([appearance="lightweight"]) .invoker:active { + border-bottom-color: ${accentForegroundActive}; + } + + :host([appearance="lightweight"]) .invoker:hover { + border-bottom-color: ${accentForegroundHover}; + } + + .disclosure[open] .invoker ~ * { + animation: fadeIn 0.5s ease-in-out; + } + + @keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } +`; diff --git a/packages/components/src/disclosure/fixtures/disclosure.html b/packages/components/src/disclosure/fixtures/disclosure.html new file mode 100644 index 00000000..3dbbbf86 --- /dev/null +++ b/packages/components/src/disclosure/fixtures/disclosure.html @@ -0,0 +1,84 @@ + +

Disclosure

+

+ The Flash (or simply Flash) is the name of several superheroes appearing in American + comic books published by DC Comics. +

+ + ⚡️ +
+ Created by writer Gardner Fox and artist Harry Lampert, the original Flash first + appeared in Flash Comics #1 (cover date January 1940/release month November 1939). + Nicknamed the "Scarlet Speedster", all incarnations of the Flash possess "super + speed", which includes the ability to run, move, and think extremely fast, use + superhuman reflexes, and seemingly violate certain laws of physics. +
+
+
+
+ +

Default expanded

+ +
+ Green Arrow is a fictional superhero who appears in comic books published by DC + Comics. Created by Mort Weisinger and designed by George Papp, he first appeared + in More Fun Comics #73 in November 1941. His real name is Oliver Jonas Queen, a + wealthy businessman and owner of Queen Industries who is also a well-known + celebrity in Star City. +
+
+
+
+ +

Helper methods (toggle(), show(), hide())

+ + +
+ Supergirl is an American superhero television series developed by Ali Adler, Greg + Berlanti and Andrew Kreisberg that originally aired on CBS and premiered on + October 26, 2015. It is based on the DC Comics character Supergirl, created by + Otto Binder and Al Plastino, and stars Melissa Benoist in the title role. +
+
+
+
+
+ + Toggle + +  + + Show + +  + + Hide + +
+
+ +

With lightweight (also extra slots)

+ + 👩🏻‍🦳 + Read about White Canary +
+ Sara Lance, also known by her alter-ego White Canary, is a fictional character in + The CW's Arrowverse franchise, first introduced in the 2012 pilot episode of the + television series Arrow, and later starring in Legends of Tomorrow. The character + is an original character to the television series, created by Greg Berlanti, Marc + Guggenheim and Andrew Kreisberg, but incorporates character and plot elements of + the DC Comics character Black Canary. Sara Lance was originally portrayed by + Jacqueline MacInnes Wood in the pilot episode, and has since been continually + portrayed by Caity Lotz. Sara initially goes by the moniker of The Canary, a + translation of her Arabic League of Assassins name الطائر الصافر (Ta-er + al-Sahfer), which translates to "Whistling Bird". She later adopts the code name + of White Canary before joining the Legends of Tomorrow. +
+
diff --git a/packages/components/src/disclosure/index.ts b/packages/components/src/disclosure/index.ts new file mode 100644 index 00000000..6ad4e715 --- /dev/null +++ b/packages/components/src/disclosure/index.ts @@ -0,0 +1,101 @@ +import { attr } from "@microsoft/fast-element"; +import { + Disclosure as FoundationDisclosure, + disclosureTemplate as template, +} from "@microsoft/fast-foundation"; +import { disclosureStyles as styles } from "./disclosure.styles.js"; +/** + * Types of anchor appearance. + * @public + */ +export type DisclosureAppearance = "accent" | "lightweight"; + +/** + * @internal + */ +export class Disclosure extends FoundationDisclosure { + /** + * Disclosure default height + */ + private height: number = 0; + /** + * Disclosure height after it's expanded + */ + private totalHeight: number = 0; + + public connectedCallback(): void { + super.connectedCallback(); + if (!this.appearance) { + this.appearance = "accent"; + } + } + + /** + * The appearance the anchor should have. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance?: DisclosureAppearance; + public appearanceChanged( + oldValue: DisclosureAppearance, + newValue: DisclosureAppearance + ): void { + if (oldValue !== newValue) { + this.classList.add(newValue); + this.classList.remove(oldValue); + } + } + + /** + * Set disclosure height while transitioning + * @override + */ + protected onToggle() { + super.onToggle(); + this.details.style.setProperty("height", `${this.disclosureHeight}px`); + } + + /** + * Calculate disclosure height before and after expanded + * @override + */ + protected setup() { + super.setup(); + + const getCurrentHeight = () => this.details.getBoundingClientRect().height; + this.show(); + this.totalHeight = getCurrentHeight(); + this.hide(); + this.height = getCurrentHeight(); + + if (this.expanded) { + this.show(); + } + } + + get disclosureHeight(): number { + return this.expanded ? this.totalHeight : this.height; + } +} + +/** + * A function that returns a {@link @microsoft/fast-foundation#Disclosure} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#disclosureTemplate} + * + * + * @public + * @remarks + * Generates HTML Element: `` + * + */ +export const fastDisclosure = Disclosure.compose({ + baseName: "disclosure", + baseClass: FoundationDisclosure, + template, + styles, +}); + +export { styles as disclosureStyles }; diff --git a/packages/components/src/disclosure/scenarios/index.html b/packages/components/src/disclosure/scenarios/index.html new file mode 100644 index 00000000..bd2b7ab2 --- /dev/null +++ b/packages/components/src/disclosure/scenarios/index.html @@ -0,0 +1,17 @@ + diff --git a/packages/components/src/divider/divider.stories.ts b/packages/components/src/divider/divider.stories.ts index e0876ee0..21cad883 100644 --- a/packages/components/src/divider/divider.stories.ts +++ b/packages/components/src/divider/divider.stories.ts @@ -1,50 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import DividerTemplate from "./fixtures/divider.html"; +import "./index.js"; export default { - title: 'Components/Divider', - argTypes: { - orientation: { control: 'radio', options: ['horizontal', 'vertical'] } - }, - parameters: { - actions: { - disabled: true - } - }, - decorators: [ - story => ` - -
- ${story()} -
` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ``; + title: "Divider", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - orientation: 'horizontal' -}; - -export const Vertical: StoryObj = { render: Template.bind({}) }; -Vertical.args = { - ...Default.args, - orientation: 'vertical' -}; +export const Divider = () => DividerTemplate; diff --git a/packages/components/src/divider/divider.styles.ts b/packages/components/src/divider/divider.styles.ts new file mode 100644 index 00000000..07ff3499 --- /dev/null +++ b/packages/components/src/divider/divider.styles.ts @@ -0,0 +1,28 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { display, FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { designUnit, neutralStrokeDividerRest, strokeWidth } from "../design-tokens.js"; + +/** + * Styles for Divider + * @public + */ +export const dividerStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("block")} :host { + box-sizing: content-box; + height: 0; + margin: calc(${designUnit} * 1px) 0; + border-top: calc(${strokeWidth} * 1px) solid ${neutralStrokeDividerRest}; + border-left: none; + } + + :host([orientation="vertical"]) { + height: 100%; + margin: 0 calc(${designUnit} * 1px); + border-top: none; + border-left: calc(${strokeWidth} * 1px) solid ${neutralStrokeDividerRest}; + } + `; diff --git a/packages/components/src/divider/index.ts b/packages/components/src/divider/index.ts index fd597f99..5931a335 100644 --- a/packages/components/src/divider/index.ts +++ b/packages/components/src/divider/index.ts @@ -1,11 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - Divider, - dividerTemplate as template -} from '@microsoft/fast-foundation'; -import { dividerStyles as styles } from '@microsoft/fast-components'; +import { Divider, dividerTemplate as template } from "@microsoft/fast-foundation"; +import { dividerStyles as styles } from "./divider.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Divider} registration for configuring the component with a DesignSystem. @@ -14,12 +8,12 @@ import { dividerStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpDivider = Divider.compose({ - baseName: 'divider', - template, - styles +export const fastDivider = Divider.compose({ + baseName: "divider", + template, + styles, }); /** diff --git a/packages/components/src/flipper/fixtures/flipper.html b/packages/components/src/flipper/fixtures/flipper.html new file mode 100644 index 00000000..021ad6f6 --- /dev/null +++ b/packages/components/src/flipper/fixtures/flipper.html @@ -0,0 +1,45 @@ +

Flipper

+

Default

+ + +

Previous

+ + +

Previous with slotted content

+ + + + + + +

Next

+ + +

Next with slotted content

+ + + + + + +

With aria-hidden set to false

+ + +

Disabled

+ diff --git a/packages/components/src/flipper/flipper.stories.ts b/packages/components/src/flipper/flipper.stories.ts new file mode 100644 index 00000000..7bb4d927 --- /dev/null +++ b/packages/components/src/flipper/flipper.stories.ts @@ -0,0 +1,8 @@ +import FlipperTemplate from "./fixtures/flipper.html"; +import "./index.js"; + +export default { + title: "Flipper", +}; + +export const Flipper = () => FlipperTemplate; diff --git a/packages/components/src/flipper/flipper.styles.ts b/packages/components/src/flipper/flipper.styles.ts new file mode 100644 index 00000000..0dfc21a3 --- /dev/null +++ b/packages/components/src/flipper/flipper.styles.ts @@ -0,0 +1,168 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + disabledCursor, + display, + FlipperOptions, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + accentFillActive, + accentFillHover, + accentFillRest, + disabledOpacity, + focusStrokeInner, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillStealthRest, + neutralForegroundRest, + neutralStrokeRest, + strokeWidth, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +/** + * Styles for Flipper + * @public + */ +export const flipperStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("inline-flex")} :host { + width: calc(${heightNumber} * 1px); + height: calc(${heightNumber} * 1px); + justify-content: center; + align-items: center; + margin: 0; + position: relative; + fill: currentcolor; + color: ${foregroundOnAccentRest}; + background: transparent; + outline: none; + border: none; + padding: 0; + } + + :host::before { + content: ""; + background: ${accentFillRest}; + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + border-radius: 50%; + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + transition: all 0.1s ease-in-out; + } + + .next, + .previous { + position: relative; + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + display: grid; + } + + :host([disabled]) { + opacity: ${disabledOpacity}; + cursor: ${disabledCursor}; + fill: currentcolor; + color: ${neutralForegroundRest}; + pointer-events: none; + } + + :host([disabled])::before, + :host([disabled]:hover)::before, + :host([disabled]:active)::before { + background: ${neutralFillStealthRest}; + border-color: ${neutralStrokeRest}; + } + + :host(:hover) { + color: ${foregroundOnAccentHover}; + } + + :host(:hover)::before { + background: ${accentFillHover}; + border-color: ${accentFillHover}; + } + + :host(:active) { + color: ${foregroundOnAccentActive}; + } + + :host(:active)::before { + background: ${accentFillActive}; + border-color: ${accentFillActive}; + } + + :host(:${focusVisible}) { + outline: none; + } + + :host(:${focusVisible})::before { + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${focusStrokeOuter} inset, + 0 0 0 calc((${focusStrokeWidth} + ${strokeWidth}) * 1px) ${focusStrokeInner} inset; + border-color: ${focusStrokeOuter}; + } + + :host::-moz-focus-inner { + border: 0; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + background: ${SystemColors.Canvas}; + } + :host .next, + :host .previous { + color: ${SystemColors.ButtonText}; + fill: currentcolor; + } + :host::before { + background: ${SystemColors.Canvas}; + border-color: ${SystemColors.ButtonText}; + } + :host(:hover)::before { + forced-color-adjust: none; + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.ButtonText}; + opacity: 1; + } + :host(:hover) .next, + :host(:hover) .previous { + forced-color-adjust: none; + color: ${SystemColors.HighlightText}; + fill: currentcolor; + } + :host([disabled]) { + opacity: 1; + } + :host([disabled])::before, + :host([disabled]:hover)::before, + :host([disabled]) .next, + :host([disabled]) .previous { + forced-color-adjust: none; + background: ${SystemColors.Canvas}; + border-color: ${SystemColors.GrayText}; + color: ${SystemColors.GrayText}; + fill: ${SystemColors.GrayText}; + } + :host(:${focusVisible})::before { + forced-color-adjust: none; + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${SystemColors.Highlight} inset; + } + ` + ) + ); diff --git a/packages/components/src/flipper/index.ts b/packages/components/src/flipper/index.ts new file mode 100644 index 00000000..b65f0056 --- /dev/null +++ b/packages/components/src/flipper/index.ts @@ -0,0 +1,43 @@ +import { + Flipper, + FlipperOptions, + flipperTemplate as template, +} from "@microsoft/fast-foundation"; +import { flipperStyles as styles } from "./flipper.styles.js"; + +/** + * A function that returns a {@link @microsoft/fast-foundation#Flipper} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#flipperTemplate} + * + * + * @public + * @remarks + * Generates HTML Element: `` + */ +export const fastFlipper = Flipper.compose({ + baseName: "flipper", + template, + styles, + next: /* html */ ` + + + + `, + previous: /* html */ ` + + + + `, +}); + +/** + * Base class for Flipper + * @public + */ +export { Flipper }; + +export { styles as flipperStyles }; diff --git a/packages/components/src/flipper/scenarios/index.html b/packages/components/src/flipper/scenarios/index.html new file mode 100644 index 00000000..f7b4a4e0 --- /dev/null +++ b/packages/components/src/flipper/scenarios/index.html @@ -0,0 +1,39 @@ + + + + + + + diff --git a/packages/components/src/horizontal-scroll/README.md b/packages/components/src/horizontal-scroll/README.md new file mode 100644 index 00000000..6472bfdf --- /dev/null +++ b/packages/components/src/horizontal-scroll/README.md @@ -0,0 +1,4 @@ +# fast-horizontal-scroll +An implementation of a content scroller as a web-component. + +For more information view the [component specification](../../../fast-foundation/src/horizontal-scroll/horizontal-scroll.spec.md). \ No newline at end of file diff --git a/packages/components/src/horizontal-scroll/fixtures/horizontal-scroll.html b/packages/components/src/horizontal-scroll/fixtures/horizontal-scroll.html new file mode 100644 index 00000000..198f65c5 --- /dev/null +++ b/packages/components/src/horizontal-scroll/fixtures/horizontal-scroll.html @@ -0,0 +1,415 @@ +

Horizontal scroll

+ + + +

Default

+ + + Card number 1 + A button + + + Card number 2 + A button + + + Card number 3 + A button + + + Card number 4 + A button + + + Card number 5 + A button + + + Card number 6 + A button + + + Card number 7 + A button + + + Card number 8 + A button + + + Card number 9 + A button + + + Card number 10 + A button + + + Card number 11 + A button + + + Card number 12 + A button + + + Card number 13 + A button + + + Card number 14 + A button + + + Card number 15 + A button + + + Card number 16 + A button + + +

2 column layout

+ + + Card number 1 + A button + + + Card number 2 + A button + + + Card number 3 + A button + + + Card number 4 + A button + + + Card number 5 + A button + + + Card number 6 + A button + + + Card number 7 + A button + + + Card number 8 + A button + + + Card number 9 + A button + + + Card number 10 + A button + + + Card number 11 + A button + + + Card number 12 + A button + + + Card number 13 + A button + + + Card number 14 + A button + + + Card number 15 + A button + + + Card number 16 + A button + + + +

Delay loaded content

+ + +

Full width cards

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + + +

Slow scroll (200 pixels/second)

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Slow scroll (2s duration)

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Immedate scroll (speed=0)

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Immedate scroll (duration=0s)

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Right gradient

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Gradient both sides

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Varying heights and widths (default middle aligned)

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Top aligned

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Bottom aligned

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Mobile horizontal-scroll

+ + Card number 1 + Card number 2 + Card number 3 + Card number 4 + Card number 5 + Card number 6 + Card number 7 + Card number 8 + Card number 9 + Card number 10 + Card number 11 + Card number 12 + Card number 13 + Card number 14 + Card number 15 + Card number 16 + + +

Window Resize with Fast Components

+ + button number 1 + button number 2 + button number 3 + button number 4 + button number 5 + button number 6 + diff --git a/packages/components/src/horizontal-scroll/horizontal-scroll.pw.spec.ts b/packages/components/src/horizontal-scroll/horizontal-scroll.pw.spec.ts new file mode 100644 index 00000000..67733335 --- /dev/null +++ b/packages/components/src/horizontal-scroll/horizontal-scroll.pw.spec.ts @@ -0,0 +1,322 @@ +import type { HorizontalScroll as HorizontalScrollType } from "@microsoft/fast-foundation"; +import chai from "chai"; +import { ElementHandle } from "playwright"; +const { expect } = chai; + +type fastHorizontalScroll = HTMLElement & HorizontalScrollType; + +describe("FASTHorizontalScroll", function () { + const componentWidth = 400; + const itemCount = 16; + const itemHeight = 200; + const itemWidth = 120; + const itemSpacing = 5; + + beforeEach(async function () { + if (!this.page && !this.browser) { + this.skip(); + } + + await this.page.evaluateHandle( + ({ componentWidth, itemCount, itemHeight, itemSpacing, itemWidth }) => { + const element = document.createElement( + "fast-horizontal-scroll" + ) as fastHorizontalScroll; + + element.style.setProperty("width", `${componentWidth}px`); + element.style.setProperty("--scroll-item-spacing", `${itemSpacing}px`); + element.duration = "0s"; + + for (let i = 0; i <= itemCount; i++) { + const card = document.createElement("div"); + card.innerText = `card ${i}`; + card.style.setProperty("height", `${itemHeight}px`); + card.style.setProperty("width", `${itemWidth}px`); + element.appendChild(card); + } + + element.id = "HorizontalScroll1"; + + document.body.appendChild(element); + + return element; + }, + { + componentWidth, + itemCount, + itemHeight, + itemSpacing, + itemWidth, + } + ); + }); + + afterEach(async function () { + if (this.setupHandle) { + await this.setupHandle.dispose(); + } + }); + + // fastHorizontalScroll should render on the page + it("should render on the page", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + expect(element).to.exist; + }); + + it("should start in the 0 position", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + 0 + ); + }); + + it("should scroll to the beginning of the last element in full view", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await element.evaluateHandle(node => node.scrollToNext()); + + await element.waitForElementState("stable"); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + 250 + ); + }); + + it("should not scroll past the beginning", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal(0); + + await element.evaluateHandle(node => node.scrollToPrevious()); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + 0 + ); + }); + + it("should not scroll past the last in-view element", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await element.evaluateHandle(node => { + node.scrollContainer.scrollLeft = + node.scrollContainer.scrollWidth - node.scrollContainer.offsetWidth; + }); + + const scrollLeft = await element.evaluate( + node => node.scrollContainer.scrollWidth - node.scrollContainer.offsetWidth + ); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + scrollLeft + ); + + await element.evaluateHandle(node => node.scrollToNext()); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + scrollLeft + ); + }); + + it("should change scroll stop on resize", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + const doubleWidth = componentWidth * 2; + + await element.evaluateHandle(node => node.scrollToNext()); + + await element.waitForElementState("stable"); + + const firstXPos = await element.evaluate(node => node.scrollContainer.scrollLeft); + + await element.evaluateHandle(node => node.scrollToPrevious()); + + await element.waitForElementState("stable"); + + await element.evaluateHandle((node, doubleWidth) => { + node.style.setProperty("width", `${doubleWidth}px`); + }, doubleWidth); + + await element.waitForElementState("stable"); + + expect(await element.evaluate(node => node.clientWidth)).to.equal(doubleWidth); + + await element.evaluateHandle(node => node.scrollToNext()); + + await element.waitForElementState("stable"); + + const secondXPos = await element.evaluate( + node => node.scrollContainer.scrollLeft + ); + + expect(firstXPos).to.not.equal(secondXPos); + }); + + it("should enable the next flipper when content exceeds horizontal-scroll width", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + expect( + await element.$eval(".scroll-next", node => + node.classList.contains("disabled") + ) + ).to.be.false; + }); + + it("should disable the next flipper if content is less than horizontal-scroll width", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await element.evaluateHandle(node => { + while (node.childElementCount > 1) { + if (node.lastElementChild) { + node.removeChild(node.lastElementChild); + } + } + }); + + await element.waitForElementState("stable"); + + expect( + await element.evaluate(node => + node.shadowRoot?.querySelector(".scroll-next") + ?.classList.contains("disabled") + ) + ).to.be.true; + }); + + it("should disable the previous flipper by default", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + expect( + await element.evaluate(node => + node.shadowRoot?.querySelector(".scroll-prev")?.classList.contains("disabled") + ) + ).to.be.true; + }); + + it("should enable the previous flipper when content is scrolled", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await element.evaluateHandle(node => node.scrollToNext()); + + expect( + await element.$eval(".scroll-prev", node => + node.classList.contains("disabled") + ) + ).to.be.false; + }); + + it("should disable the previous flipper when scrolled back to the beginning", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await element.evaluateHandle(node => node.scrollToNext()); + + await element.waitForElementState("stable"); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + 250 + ); + + expect( + await element.$eval(".scroll-prev", node => + node?.classList.contains("disabled") + ) + ).to.be.false; + + await element.evaluateHandle(node => node.scrollToPrevious()); + + await element.waitForElementState("stable"); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + 0 + ); + + expect( + await element.$eval(".scroll-prev", node => + node.classList.contains("disabled") + ) + ).to.be.true; + }); + + it("should disable the next flipper when it reaches the end of the content", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await this.page.evaluateHandle(node => { + if (node.firstElementChild) { + node.scrollContainer.scrollLeft = + node.scrollContainer.scrollWidth - + node.scrollContainer.offsetWidth - + node.firstElementChild.clientWidth; + } + }, element); + + await element.evaluateHandle(node => node.scrollToNext()); + + await element.waitForElementState("stable"); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + await element.evaluate( + (node, componentWidth) => + node.scrollContainer.scrollWidth - componentWidth, + componentWidth + ) + ); + + expect( + await element.$eval(".scroll-next", node => + node.classList.contains("disabled") + ) + ).to.be.true; + }); + + it("should disable the previous flipper when it reaches the beginning of the content", async function () { + const element = (await this.page.waitForSelector( + "fast-horizontal-scroll" + )) as ElementHandle; + + await element.evaluateHandle(node => { + // Move the scrollLeft almost to the end + if (node.firstElementChild) { + node.scrollContainer.scrollLeft = node.firstElementChild.clientWidth; + } + + node.scrollToPrevious(); + }); + + await element.waitForElementState("stable"); + + expect(await element.evaluate(node => node.scrollContainer.scrollLeft)).to.equal( + 0 + ); + + expect( + await element.$eval(".scroll-prev", node => + node.classList.contains("disabled") + ) + ).to.be.true; + }); +}); diff --git a/packages/components/src/horizontal-scroll/horizontal-scroll.stories.ts b/packages/components/src/horizontal-scroll/horizontal-scroll.stories.ts new file mode 100644 index 00000000..874315ea --- /dev/null +++ b/packages/components/src/horizontal-scroll/horizontal-scroll.stories.ts @@ -0,0 +1,8 @@ +import HorizontalScrollTemplate from "./fixtures/horizontal-scroll.html"; +import "./index.js"; + +export default { + title: "Horizontal Scroll", +}; + +export const HorizontalScroll = () => HorizontalScrollTemplate; diff --git a/packages/components/src/horizontal-scroll/horizontal-scroll.styles.ts b/packages/components/src/horizontal-scroll/horizontal-scroll.styles.ts new file mode 100644 index 00000000..beba7e95 --- /dev/null +++ b/packages/components/src/horizontal-scroll/horizontal-scroll.styles.ts @@ -0,0 +1,140 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + display, + FoundationElementTemplate, + HorizontalScrollOptions, +} from "@microsoft/fast-foundation"; +import { DirectionalStyleSheetBehavior } from "../styles/direction.js"; + +const ltrActionsStyles = css` + .scroll-prev { + right: auto; + left: 0; + } + + .scroll.scroll-next::before, + .scroll-next .scroll-action { + left: auto; + right: 0; + } + + .scroll.scroll-next::before { + background: linear-gradient(to right, transparent, var(--scroll-fade-next)); + } + + .scroll-next .scroll-action { + transform: translate(50%, -50%); + } +`; + +const rtlActionsStyles = css` + .scroll.scroll-next { + right: auto; + left: 0; + } + + .scroll.scroll-next::before { + background: linear-gradient(to right, var(--scroll-fade-next), transparent); + left: auto; + right: 0; + } + + .scroll.scroll-prev::before { + background: linear-gradient(to right, transparent, var(--scroll-fade-previous)); + } + + .scroll-prev .scroll-action { + left: auto; + right: 0; + transform: translate(50%, -50%); + } +`; + +/** + * Styles used for the flipper container and gradient fade actions + * @public + */ +export const ActionsStyles = css` + .scroll-area { + position: relative; + } + + div.scroll-view { + overflow-x: hidden; + } + + .scroll { + bottom: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + user-select: none; + width: 100px; + } + + .scroll.disabled { + display: none; + } + + .scroll::before, + .scroll-action { + left: 0; + position: absolute; + } + + .scroll::before { + background: linear-gradient(to right, var(--scroll-fade-previous), transparent); + content: ""; + display: block; + height: 100%; + width: 100%; + } + + .scroll-action { + pointer-events: auto; + right: auto; + top: 50%; + transform: translate(-50%, -50%); + } +`.withBehaviors(new DirectionalStyleSheetBehavior(ltrActionsStyles, rtlActionsStyles)); + +/** + * Horizontal Scroll styles + * @public + */ +export const horizontalScrollStyles: FoundationElementTemplate< + ElementStyles, + HorizontalScrollOptions +> = (context, definition) => css` + ${display("block")} :host { + --scroll-align: center; + --scroll-item-spacing: 5px; + contain: layout; + position: relative; + } + + .scroll-view { + overflow-x: auto; + scrollbar-width: none; + } + + ::-webkit-scrollbar { + display: none; + } + + .content-container { + align-items: var(--scroll-align); + display: inline-flex; + flex-wrap: nowrap; + position: relative; + } + + .content-container ::slotted(*) { + margin-right: var(--scroll-item-spacing); + } + + .content-container ::slotted(*:last-child) { + margin-right: 0; + } +`; diff --git a/packages/components/src/horizontal-scroll/index.ts b/packages/components/src/horizontal-scroll/index.ts new file mode 100644 index 00000000..45d95714 --- /dev/null +++ b/packages/components/src/horizontal-scroll/index.ts @@ -0,0 +1,58 @@ +import { html } from "@microsoft/fast-element"; +import { + Flipper, + HorizontalScroll as FoundationHorizontalScroll, + HorizontalScrollOptions, + horizontalScrollTemplate as template, +} from "@microsoft/fast-foundation"; +import { + ActionsStyles, + horizontalScrollStyles as styles, +} from "./horizontal-scroll.styles.js"; + +/** + * @internal + */ +export class HorizontalScroll extends FoundationHorizontalScroll { + /** + * @public + */ + public connectedCallback(): void { + super.connectedCallback(); + + if (this.view !== "mobile") { + this.$fastController.addStyles(ActionsStyles); + } + } +} + +/** + * A function that returns a {@link @microsoft/fast-foundation#HorizontalScroll} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#horizontalScrollTemplate} + * + * + * @public + * @remarks + * Generates HTML Element: `` + */ +export const fastHorizontalScroll = HorizontalScroll.compose({ + baseName: "horizontal-scroll", + baseClass: FoundationHorizontalScroll, + template, + styles, + nextFlipper: context => html` + <${context.tagFor(Flipper)} + @click="${x => x.scrollToNext()}" + aria-hidden="${x => x.flippersHiddenFromAT}" + > + `, + previousFlipper: context => html` + <${context.tagFor(Flipper)} + @click="${x => x.scrollToPrevious()}" + direction="previous" + aria-hidden="${x => x.flippersHiddenFromAT}" + > + `, +}); + +export { ActionsStyles, styles as horizontalScrollStyles }; diff --git a/packages/components/src/index-rollup.ts b/packages/components/src/index-rollup.ts index 7eb709cb..21227d7d 100644 --- a/packages/components/src/index-rollup.ts +++ b/packages/components/src/index-rollup.ts @@ -1,16 +1,14 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +// TODO: Is exporting Foundation still necessary with the updated API's? +// export * from "@microsoft/fast-element"; +import { allComponents } from "./custom-elements.js"; +import { provideFASTDesignSystem } from "./fast-design-system.js"; -import { allComponents } from './custom-elements'; -import { provideJupyterDesignSystem } from './jupyter-design-system'; - -export * from './index'; +export * from "./index.js"; /** - * The global Jupyter Design System. + * The global FAST Design System. * @remarks * Only available if the components are added through a script tag * rather than a module/build system. */ -export const JupyterDesignSystem = - provideJupyterDesignSystem().register(allComponents); +export const FASTDesignSystem = provideFASTDesignSystem().register(allComponents); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 46791d77..eeab247d 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,48 +1,54 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -export { - addJupyterLabThemeChangeListener, - applyJupyterTheme -} from './utilities/theme/applyTheme'; - -export * from './color'; -export * from './design-tokens'; -export * from './jupyter-design-system'; -export * from './custom-elements'; - -// Export components and classes -export * from './accordion/index'; -export * from './accordion-item/index'; -export * from './anchor/index'; -export * from './anchored-region/index'; -export * from './avatar/index'; -export * from './badge/index'; -export * from './breadcrumb/index'; -export * from './breadcrumb-item/index'; -export * from './button/index'; -export * from './card/index'; -export * from './checkbox/index'; -export * from './combobox/index'; -export * from './date-field/index'; -export * from './data-grid/index'; -export * from './dialog/index'; -export * from './divider/index'; -export * from './listbox/index'; -export * from './menu/index'; -export * from './menu-item/index'; -export * from './number-field/index'; -export * from './option/index'; -export * from './progress/index'; -export * from './radio/index'; -export * from './radio-group/index'; -export * from './search/index'; -export * from './select/index'; -export * from './slider-label/index'; -export * from './tab-panel/index'; -export * from './tab/index'; -export * from './tabs/index'; -export * from './text-area/index'; -export * from './text-field/index'; -export * from './toolbar/index'; -export * from './tooltip/index'; +/** + * Export all custom element definitions. + */ +export * from "./custom-elements.js"; +export * from "./fast-design-system.js"; +export * from "./accordion/index.js"; +export * from "./anchor/index.js"; +export * from "./anchored-region/index.js"; +export * from "./avatar/index.js"; +export * from "./badge/index.js"; +export * from "./breadcrumb/index.js"; +export * from "./breadcrumb-item/index.js"; +export * from "./button/index.js"; +export * from "./calendar/index.js"; +export * from "./card/index.js"; +export * from "./checkbox/index.js"; +export * from "./combobox/index.js"; +export * from "./data-grid/index.js"; +export * from "./design-system-provider/index.js"; +export { Palette, PaletteRGB } from "./color/palette.js"; +export { InteractiveSwatchSet } from "./color/recipe.js"; +export { Swatch, SwatchRGB } from "./color/swatch.js"; +export { isDark } from "./color/utilities/is-dark.js"; +export { StandardLuminance } from "./color/utilities/base-layer-luminance.js"; +export * from "./design-tokens.js"; +export * from "./dialog/index.js"; +export * from "./disclosure/index.js"; +export * from "./divider/index.js"; +export * from "./flipper/index.js"; +export * from "./horizontal-scroll/index.js"; +export * from "./listbox/index.js"; +export * from "./listbox-option/index.js"; +export * from "./menu/index.js"; +export * from "./menu-item/index.js"; +export * from "./number-field/index.js"; +export * from "./picker/index.js"; +export * from "./progress/index.js"; +export * from "./progress-ring/index.js"; +export * from "./radio/index.js"; +export * from "./radio-group/index.js"; +export * from "./search/index.js"; +export * from "./select/index.js"; +export * from "./skeleton/index.js"; +export * from "./slider/index.js"; +export * from "./slider-label/index.js"; +export * from "./styles/direction.js"; +export * from "./switch/index.js"; +export * from "./tabs/index.js"; +export * from "./text-area/index.js"; +export * from "./text-field/index.js"; +export * from "./toolbar/index.js"; +export * from "./tooltip/index.js"; +export * from "./tree-view/index.js"; +export * from "./tree-item/index.js"; diff --git a/packages/components/src/listbox/index.ts b/packages/components/src/listbox/index.ts index 69b470eb..d9160ffe 100644 --- a/packages/components/src/listbox/index.ts +++ b/packages/components/src/listbox/index.ts @@ -1,32 +1,73 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - +import { css, ElementStyles } from "@microsoft/fast-element"; import { - ListboxElement, - listboxTemplate as template -} from '@microsoft/fast-foundation'; -import { listboxStyles as styles } from './listbox.styles'; + ListboxElement as FoundationListboxElement, + listboxTemplate as template, +} from "@microsoft/fast-foundation"; +import { listboxStyles as styles } from "./listbox.styles.js"; /** - * The Jupyter listbox Custom Element. Implements, {@link @microsoft/fast-foundation#Listbox} - * {@link @microsoft/fast-foundation#ListboxTemplate} - * + * Base class for Listbox. * * @public - * @remarks - * HTML Element: \ - * */ -export const jpListbox = ListboxElement.compose({ - baseName: 'listbox', - template, - styles -}); +export class Listbox extends FoundationListboxElement { + /** + * An internal stylesheet to hold calculated CSS custom properties. + * + * @internal + */ + private computedStylesheet?: ElementStyles; + + /** + * Updates the component dimensions when the size property is changed. + * + * @param prev - the previous size value + * @param next - the current size value + * + * @internal + */ + protected sizeChanged(prev: number | undefined, next: number): void { + super.sizeChanged(prev, next); + this.updateComputedStylesheet(); + } + + /** + * Updates an internal stylesheet with calculated CSS custom properties. + * + * @internal + */ + protected updateComputedStylesheet(): void { + if (this.computedStylesheet) { + this.$fastController.removeStyles(this.computedStylesheet); + } + + const listboxSize = `${this.size}`; + + this.computedStylesheet = css` + :host { + --size: ${listboxSize}; + } + `; + + this.$fastController.addStyles(this.computedStylesheet); + } +} /** - * Base class for ListBox + * A function that returns a {@link @microsoft/fast-foundation#ListboxElement} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#listboxTemplate} + * + * @remarks + * Generates HTML Element: `` + * * @public + * */ -export { ListboxElement }; +export const fastListbox = Listbox.compose({ + baseName: "listbox", + baseClass: FoundationListboxElement, + template, + styles, +}); export { styles as listboxStyles }; diff --git a/packages/components/src/listbox/listbox.pw.spec.ts b/packages/components/src/listbox/listbox.pw.spec.ts new file mode 100644 index 00000000..27310ec6 --- /dev/null +++ b/packages/components/src/listbox/listbox.pw.spec.ts @@ -0,0 +1,103 @@ +import type { + ListboxElement as FASTListboxType, + ListboxOption as FASTOption, +} from "@microsoft/fast-foundation"; +import type { ElementHandle } from "playwright"; +import chai from "chai"; + +const { expect } = chai; + +type FASTListbox = HTMLElement & FASTListboxType; + +describe("FASTListbox", function () { + beforeEach(async function () { + if (!this.page && !this.browser) { + this.skip(); + } + + this.documentHandle = await this.page.evaluateHandle(() => document); + + this.setupHandle = await this.page.evaluateHandle( + (document) => { + const element = document.createElement("fast-listbox") as FASTListbox; + + for (let i = 1; i <= 3; i++) { + const option = document.createElement("fast-option") as FASTOption; + option.value = `${i}`; + option.textContent = `option ${i}`; + element.appendChild(option); + } + + document.body.appendChild(element) + }, + this.documentHandle + ); + }); + + afterEach(async function () { + if (this.setupHandle) { + await this.setupHandle.dispose(); + } + }); + + // FASTListbox should render on the page + it("should render on the page", async function () { + const element = await this.page.waitForSelector("fast-listbox"); + + expect(element).to.exist; + }); + + describe("should change the `selectedIndex` when focused and receives keyboard interaction", function () { + it("via arrow down key", async function () { + const element = (await this.page.waitForSelector( + "fast-listbox" + )) as ElementHandle; + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(-1); + + await element.press("ArrowDown"); + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(0); + }); + + it("via arrow up key", async function () { + const element = (await this.page.waitForSelector( + "fast-listbox" + )) as ElementHandle; + + await element.evaluate(node => (node.selectedIndex = 1)); + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(1); + + await element.press("ArrowUp"); + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(0); + }); + + it("via home key", async function () { + const element = (await this.page.waitForSelector( + "fast-listbox" + )) as ElementHandle; + + await element.evaluate(node => (node.selectedIndex = 2)); + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(2); + + await element.press("Home"); + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(0); + }); + + it("via end key", async function () { + const element = (await this.page.waitForSelector( + "fast-listbox" + )) as ElementHandle; + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(-1); + + await element.press("End"); + + expect(await element.evaluate(node => node.selectedIndex)).to.equal(2); + }); + }); +}); diff --git a/packages/components/src/listbox/listbox.stories.ts b/packages/components/src/listbox/listbox.stories.ts index ec6bf977..6d5bab16 100644 --- a/packages/components/src/listbox/listbox.stories.ts +++ b/packages/components/src/listbox/listbox.stories.ts @@ -1,47 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import Base from "./fixtures/base.html"; +import Multiselect from "./fixtures/multiselect.html"; export default { - title: 'Components/Listbox', - argTypes: { - isDisabled: { control: 'boolean' }, - multiple: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - Option 1 - Option 2 - Option 3 - `; + title: "Listbox", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - isDisabled: false, - multiple: false -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true -}; +export const Listbox = () => Base; +export const ListboxMultiselect = () => Multiselect; diff --git a/packages/components/src/listbox/listbox.styles.ts b/packages/components/src/listbox/listbox.styles.ts index d8a054e9..369fab4f 100644 --- a/packages/components/src/listbox/listbox.styles.ts +++ b/packages/components/src/listbox/listbox.styles.ts @@ -1,48 +1,43 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import type { ElementStyles } from '@microsoft/fast-element'; -import { css } from '@microsoft/fast-element'; -import type { FoundationElementTemplate } from '@microsoft/fast-foundation'; +import type { ElementStyles } from "@microsoft/fast-element"; +import { css } from "@microsoft/fast-element"; +import type { FoundationElementTemplate } from "@microsoft/fast-foundation"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - ListboxElement, - ListboxOption -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + ListboxElement, + ListboxOption, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - controlCornerRadius, - designUnit, - disabledOpacity, - fillColor, - focusStrokeOuter, - focusStrokeWidth, - neutralStrokeRest, - strokeWidth -} from '@microsoft/fast-components'; -import { heightNumber } from '../styles/size'; + controlCornerRadius, + designUnit, + disabledOpacity, + fillColor, + focusStrokeOuter, + focusStrokeWidth, + neutralStrokeRest, + strokeWidth, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/size.js"; /** * Styles for Listbox * @public */ export const listboxStyles: FoundationElementTemplate = ( - context, - definition + context, + definition ) => { - const ListboxOptionTag = context.tagFor(ListboxOption); - const hostContext = - context.name === context.tagFor(ListboxElement) ? '' : '.listbox'; + const ListboxOptionTag = context.tagFor(ListboxOption); + const hostContext = context.name === context.tagFor(ListboxElement) ? "" : ".listbox"; - // The expression interpolations present in this block cause Prettier to generate - // various formatting bugs. - // prettier-ignore - return css` - ${!hostContext ? display('inline-flex') : ''} + // The expression interpolations present in this block cause Prettier to generate + // various formatting bugs. + // prettier-ignore + return css` + ${!hostContext ? display("inline-flex") : ""} :host ${hostContext} { background: ${fillColor}; @@ -54,10 +49,6 @@ export const listboxStyles: FoundationElementTemplate = ( } ${!hostContext ? css` - :host(:${focusVisible}:not([disabled])) { - outline: none; - } - :host(:focus-within:not([disabled])) { border-color: ${focusStrokeOuter}; box-shadow: 0 0 0 @@ -70,9 +61,9 @@ export const listboxStyles: FoundationElementTemplate = ( opacity: ${disabledOpacity}; pointer-events: none; } - ` : ''} + ` : ``} - ${hostContext || ':host([size])'} { + ${hostContext || `:host([size])`} { max-height: calc( (var(--size) * ${heightNumber} + (${designUnit} * ${strokeWidth} * 2)) * 1px ); diff --git a/packages/components/src/menu-item/index.ts b/packages/components/src/menu-item/index.ts index 63d74d36..88ca7879 100644 --- a/packages/components/src/menu-item/index.ts +++ b/packages/components/src/menu-item/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - MenuItem, - MenuItemOptions, - menuItemTemplate as template -} from '@microsoft/fast-foundation'; -import { menuItemStyles as styles } from './menu-item.styles'; + MenuItem, + MenuItemOptions, + menuItemTemplate as template, +} from "@microsoft/fast-foundation"; +import { menuItemStyles as styles } from "./menu-item.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#MenuItem} registration for configuring the component with a DesignSystem. @@ -15,41 +12,41 @@ import { menuItemStyles as styles } from './menu-item.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpMenuItem = MenuItem.compose({ - baseName: 'menu-item', - template, - styles, - checkboxIndicator: /* html */ ` - - - +export const fastMenuItem = MenuItem.compose({ + baseName: "menu-item", + template, + styles, + checkboxIndicator: /* html */ ` + + + + `, + expandCollapseGlyph: /* html */ ` + + + `, - expandCollapseGlyph: /* html */ ` - - - + radioIndicator: /* html */ ` + `, - radioIndicator: /* html */ ` - - ` }); /** diff --git a/packages/components/src/menu-item/menu-item.stories.ts b/packages/components/src/menu-item/menu-item.stories.ts index f0ea566d..9b3a53df 100644 --- a/packages/components/src/menu-item/menu-item.stories.ts +++ b/packages/components/src/menu-item/menu-item.stories.ts @@ -1,69 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import MenuItemTemplate from "./fixtures/menu-item.html"; +import "./index.js"; export default { - title: 'Components/Menu Item', - argTypes: { - role: { - control: 'radio', - options: ['menuitem', 'menuitemcheckbox', 'menuitemradio'] - }, - checked: { control: 'boolean' }, - disabled: { control: 'boolean' }, - startIcon: { control: 'boolean' }, - endIcon: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - ${args.startIcon ? getFaIcon('folder', 'start') : ''} - Menu item - ${args.endIcon ? getFaIcon('robot', 'end') : ''} - `; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - role: 'menuitem', - checked: false, - disabled: false, - startIcon: false, - endIcon: false + title: "Menu Item", }; -export const Disabled: StoryObj = { render: Template.bind({}) }; -Disabled.args = { - ...Default.args, - disabled: true -}; - -export const Checkbox: StoryObj = { render: Template.bind({}) }; -Checkbox.args = { - ...Default.args, - role: 'menuitemcheckbox' -}; - -export const Radio: StoryObj = { render: Template.bind({}) }; -Radio.args = { - ...Default.args, - role: 'menuitemradio' -}; +export const MenuItem = () => MenuItemTemplate; diff --git a/packages/components/src/menu-item/menu-item.styles.ts b/packages/components/src/menu-item/menu-item.styles.ts index 734fd281..20347ffc 100644 --- a/packages/components/src/menu-item/menu-item.styles.ts +++ b/packages/components/src/menu-item/menu-item.styles.ts @@ -1,365 +1,359 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - MenuItemOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + MenuItemOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillRest, - bodyFont, - controlCornerRadius, - designUnit, - DirectionalStyleSheetBehavior, - disabledOpacity, - focusStrokeOuter, - focusStrokeWidth, - foregroundOnAccentRest, - neutralFillStealthRest, - neutralForegroundHint, - neutralForegroundRest, - neutralLayer2, - neutralLayer3, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentRest, + neutralFillRest, + neutralFillStealthActive, + neutralFillStealthFocus, + neutralFillStealthHover, + neutralFillStealthRest, + neutralForegroundHint, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { DirectionalStyleSheetBehavior, heightNumber } from "../styles/index.js"; /** * Styles for Menu item * @public */ -export const menuItemStyles: FoundationElementTemplate< - ElementStyles, - MenuItemOptions -> = (context, definition) => - css` - ${display('grid')} :host { - contain: layout; - overflow: visible; - font-family: ${bodyFont}; - outline: none; - box-sizing: border-box; - height: calc(${heightNumber} * 1px); - grid-template-columns: minmax(42px, auto) 1fr minmax(42px, auto); - grid-template-rows: auto; - justify-items: center; - align-items: center; - padding: 0; - margin: 0 calc(${designUnit} * 1px); - white-space: nowrap; - color: ${neutralForegroundRest}; - fill: currentcolor; - cursor: pointer; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - border-radius: calc(${controlCornerRadius} * 1px); - border: calc(${focusStrokeWidth} * 1px) solid transparent; - } - - :host(:hover) { - position: relative; - z-index: 1; - } - - :host(.indent-0) { - grid-template-columns: auto 1fr minmax(42px, auto); - } - :host(.indent-0) .content { - grid-column: 1; - grid-row: 1; - margin-inline-start: 10px; - } - :host(.indent-0) .expand-collapse-glyph-container { - grid-column: 5; - grid-row: 1; - } - :host(.indent-2) { - grid-template-columns: - minmax(42px, auto) minmax(42px, auto) 1fr minmax(42px, auto) - minmax(42px, auto); - } - :host(.indent-2) .content { - grid-column: 3; - grid-row: 1; - margin-inline-start: 10px; - } - :host(.indent-2) .expand-collapse-glyph-container { - grid-column: 5; - grid-row: 1; - } - :host(.indent-2) .start { - grid-column: 2; - } - :host(.indent-2) .end { - grid-column: 4; - } - - :host(:${focusVisible}) { - border-color: ${focusStrokeOuter}; - background: ${neutralLayer3}; - color: ${neutralForegroundRest}; - } - - :host(:hover) { - background: ${neutralLayer3}; - color: ${neutralForegroundRest}; - } - - :host([aria-checked='true']), - :host(:active), - :host(.expanded) { - background: ${neutralLayer2}; - color: ${neutralForegroundRest}; - } - - :host([disabled]) { - cursor: ${disabledCursor}; - opacity: ${disabledOpacity}; - } - - :host([disabled]:hover) { - color: ${neutralForegroundRest}; - fill: currentcolor; - background: ${neutralFillStealthRest}; - } - - :host([disabled]:hover) .start, - :host([disabled]:hover) .end, - :host([disabled]:hover)::slotted(svg) { - fill: ${neutralForegroundRest}; - } - - .expand-collapse-glyph { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - width: 16px; - height: 16px; - fill: currentcolor; - } - - .content { - grid-column-start: 2; - justify-self: start; - overflow: hidden; - text-overflow: ellipsis; - } - - .start, - .end { - display: flex; - justify-content: center; - } - - ::slotted(svg) { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - width: 16px; - height: 16px; - } - - :host(:hover) .start, - :host(:hover) .end, - :host(:hover)::slotted(svg), - :host(:active) .start, - :host(:active) .end, - :host(:active)::slotted(svg) { - fill: ${neutralForegroundRest}; - } - - :host(.indent-0[aria-haspopup='menu']) { - display: grid; - grid-template-columns: minmax(42px, auto) auto 1fr minmax(42px, auto) minmax( - 42px, - auto - ); - align-items: center; - min-height: 32px; - } - - :host(.indent-1[aria-haspopup='menu']), - :host(.indent-1[role='menuitemcheckbox']), - :host(.indent-1[role='menuitemradio']) { - display: grid; - grid-template-columns: minmax(42px, auto) auto 1fr minmax(42px, auto) minmax( - 42px, - auto - ); - align-items: center; - min-height: 32px; - } - - :host(.indent-2:not([aria-haspopup='menu'])) .end { - grid-column: 5; - } - - :host .input-container, - :host .expand-collapse-glyph-container { - display: none; - } - - :host([aria-haspopup='menu']) .expand-collapse-glyph-container, - :host([role='menuitemcheckbox']) .input-container, - :host([role='menuitemradio']) .input-container { - display: grid; - margin-inline-end: 10px; - } - - :host([aria-haspopup='menu']) .content, - :host([role='menuitemcheckbox']) .content, - :host([role='menuitemradio']) .content { - grid-column-start: 3; - } - - :host([aria-haspopup='menu'].indent-0) .content { - grid-column-start: 1; - } - - :host([aria-haspopup='menu']) .end, - :host([role='menuitemcheckbox']) .end, - :host([role='menuitemradio']) .end { - grid-column-start: 4; - } - - :host .expand-collapse, - :host .checkbox, - :host .radio { - display: flex; - align-items: center; - justify-content: center; - position: relative; - width: 20px; - height: 20px; - box-sizing: border-box; - outline: none; - margin-inline-start: 10px; - } - - :host .checkbox, - :host .radio { - border: calc(${strokeWidth} * 1px) solid ${neutralForegroundRest}; - } - - :host([aria-checked='true']) .checkbox, - :host([aria-checked='true']) .radio { - background: ${accentFillRest}; - border-color: ${accentFillRest}; - } - - :host .checkbox { - border-radius: calc(${controlCornerRadius} * 1px); - } - - :host .radio { - border-radius: 999px; - } - - :host .checkbox-indicator, - :host .radio-indicator, - :host .expand-collapse-indicator, - ::slotted([slot='checkbox-indicator']), - ::slotted([slot='radio-indicator']), - ::slotted([slot='expand-collapse-indicator']) { - display: none; - } - - ::slotted([slot='end']:not(svg)) { - margin-inline-end: 10px; - color: ${neutralForegroundHint}; - } - - :host([aria-checked='true']) .checkbox-indicator, - :host([aria-checked='true']) ::slotted([slot='checkbox-indicator']) { - width: 100%; - height: 100%; - display: block; - fill: ${foregroundOnAccentRest}; - pointer-events: none; - } - - :host([aria-checked='true']) .radio-indicator { - position: absolute; - top: 4px; - left: 4px; - right: 4px; - bottom: 4px; - border-radius: 999px; - display: block; - background: ${foregroundOnAccentRest}; - pointer-events: none; - } - - :host([aria-checked='true']) ::slotted([slot='radio-indicator']) { - display: block; - pointer-events: none; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host { - border-color: transparent; - color: ${SystemColors.ButtonText}; - forced-color-adjust: none; - } - - :host(:hover) { - background: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } - - :host(:hover) .start, - :host(:hover) .end, - :host(:hover)::slotted(svg), - :host(:active) .start, - :host(:active) .end, - :host(:active)::slotted(svg) { - fill: ${SystemColors.HighlightText}; - } - - :host(.expanded) { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } - - :host(:${focusVisible}) { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.ButtonText}; - box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) inset - ${SystemColors.HighlightText}; - color: ${SystemColors.HighlightText}; - fill: currentcolor; - } - - :host([disabled]), - :host([disabled]:hover), - :host([disabled]:hover) .start, - :host([disabled]:hover) .end, - :host([disabled]:hover)::slotted(svg) { - background: ${SystemColors.Canvas}; - color: ${SystemColors.GrayText}; - fill: currentcolor; - opacity: 1; - } - - :host .expanded-toggle, - :host .checkbox, - :host .radio { - border-color: ${SystemColors.ButtonText}; - background: ${SystemColors.HighlightText}; - } - - :host([checked='true']) .checkbox, - :host([checked='true']) .radio { - background: ${SystemColors.HighlightText}; - border-color: ${SystemColors.HighlightText}; - } - - :host(:hover) .expanded-toggle, +export const menuItemStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("grid")} :host { + contain: layout; + overflow: visible; + font-family: ${bodyFont}; + outline: none; + box-sizing: border-box; + height: calc(${heightNumber} * 1px); + grid-template-columns: minmax(42px, auto) 1fr minmax(42px, auto); + grid-template-rows: auto; + justify-items: center; + align-items: center; + padding: 0; + margin: 0 calc(${designUnit} * 1px); + white-space: nowrap; + background: ${neutralFillStealthRest}; + color: ${neutralForegroundRest}; + fill: currentcolor; + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${focusStrokeWidth} * 1px) solid transparent; + } + + :host(:hover) { + position: relative; + z-index: 1; + } + + :host(.indent-0) { + grid-template-columns: auto 1fr minmax(42px, auto); + } + :host(.indent-0) .content { + grid-column: 1; + grid-row: 1; + margin-inline-start: 10px; + } + :host(.indent-0) .expand-collapse-glyph-container { + grid-column: 5; + grid-row: 1; + } + :host(.indent-2) { + grid-template-columns: minmax(42px, auto) minmax(42px, auto) 1fr minmax(42px, auto) minmax(42px, auto); + } + :host(.indent-2) .content { + grid-column: 3; + grid-row: 1; + margin-inline-start: 10px; + } + :host(.indent-2) .expand-collapse-glyph-container { + grid-column: 5; + grid-row: 1; + } + :host(.indent-2) .start { + grid-column: 2; + } + :host(.indent-2) .end { + grid-column: 4; + } + + :host(:${focusVisible}) { + border-color: ${focusStrokeOuter}; + background: ${neutralFillStealthFocus}; + color: ${neutralForegroundRest}; + } + + :host(:hover) { + background: ${neutralFillStealthHover}; + color: ${neutralForegroundRest}; + } + + :host(:active) { + background: ${neutralFillStealthActive}; + } + + :host([aria-checked="true"]), + :host(.expanded) { + background: ${neutralFillRest}; + color: ${neutralForegroundRest}; + } + + :host([disabled]) { + cursor: ${disabledCursor}; + opacity: ${disabledOpacity}; + } + + :host([disabled]:hover) { + color: ${neutralForegroundRest}; + fill: currentcolor; + background: ${neutralFillStealthRest}; + } + + :host([disabled]:hover) .start, + :host([disabled]:hover) .end, + :host([disabled]:hover)::slotted(svg) { + fill: ${neutralForegroundRest}; + } + + .expand-collapse-glyph { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + fill: currentcolor; + } + + .content { + grid-column-start: 2; + justify-self: start; + overflow: hidden; + text-overflow: ellipsis; + } + + .start, + .end { + display: flex; + justify-content: center; + } + + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + } + + :host(:hover) .start, + :host(:hover) .end, + :host(:hover)::slotted(svg), + :host(:active) .start, + :host(:active) .end, + :host(:active)::slotted(svg) { + fill: ${neutralForegroundRest}; + } + + :host(.indent-0[aria-haspopup="menu"]) { + display: grid; + grid-template-columns: minmax(42px, auto) auto 1fr minmax(42px, auto) minmax(42px, auto); + align-items: center; + min-height: 32px; + } + + :host(.indent-1[aria-haspopup="menu"]), + :host(.indent-1[role="menuitemcheckbox"]), + :host(.indent-1[role="menuitemradio"]) { + display: grid; + grid-template-columns: minmax(42px, auto) auto 1fr minmax(42px, auto) minmax(42px, auto); + align-items: center; + min-height: 32px; + } + + :host(.indent-2:not([aria-haspopup="menu"])) .end { + grid-column: 5; + } + + :host .input-container, + :host .expand-collapse-glyph-container { + display: none; + } + + :host([aria-haspopup="menu"]) .expand-collapse-glyph-container, + :host([role="menuitemcheckbox"]) .input-container, + :host([role="menuitemradio"]) .input-container { + display: grid; + margin-inline-end: 10px; + } + + :host([aria-haspopup="menu"]) .content, + :host([role="menuitemcheckbox"]) .content, + :host([role="menuitemradio"]) .content { + grid-column-start: 3; + } + + :host([aria-haspopup="menu"].indent-0) .content { + grid-column-start: 1; + } + + :host([aria-haspopup="menu"]) .end, + :host([role="menuitemcheckbox"]) .end, + :host([role="menuitemradio"]) .end { + grid-column-start: 4; + } + + :host .expand-collapse, + :host .checkbox, + :host .radio { + display: flex; + align-items: center; + justify-content: center; + position: relative; + width: 20px; + height: 20px; + box-sizing: border-box; + outline: none; + margin-inline-start: 10px; + } + + :host .checkbox, + :host .radio { + border: calc(${strokeWidth} * 1px) solid ${neutralForegroundRest}; + } + + :host([aria-checked="true"]) .checkbox, + :host([aria-checked="true"]) .radio { + background: ${accentFillRest}; + border-color: ${accentFillRest}; + } + + :host .checkbox { + border-radius: calc(${controlCornerRadius} * 1px); + } + + :host .radio { + border-radius: 999px; + } + + :host .checkbox-indicator, + :host .radio-indicator, + :host .expand-collapse-indicator, + ::slotted([slot="checkbox-indicator"]), + ::slotted([slot="radio-indicator"]), + ::slotted([slot="expand-collapse-indicator"]) { + display: none; + } + + ::slotted([slot="end"]:not(svg)) { + margin-inline-end: 10px; + color: ${neutralForegroundHint} + } + + :host([aria-checked="true"]) .checkbox-indicator, + :host([aria-checked="true"]) ::slotted([slot="checkbox-indicator"]) { + width: 100%; + height: 100%; + display: block; + fill: ${foregroundOnAccentRest}; + pointer-events: none; + } + + :host([aria-checked="true"]) .radio-indicator { + position: absolute; + top: 4px; + left: 4px; + right: 4px; + bottom: 4px; + border-radius: 999px; + display: block; + background: ${foregroundOnAccentRest}; + pointer-events: none; + } + + :host([aria-checked="true"]) ::slotted([slot="radio-indicator"]) { + display: block; + pointer-events: none; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + border-color: transparent; + color: ${SystemColors.ButtonText}; + forced-color-adjust: none; + } + + :host(:hover) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + :host(:hover) .start, + :host(:hover) .end, + :host(:hover)::slotted(svg), + :host(:active) .start, + :host(:active) .end, + :host(:active)::slotted(svg) { + fill: ${SystemColors.HighlightText}; + } + + :host(.expanded) { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + :host(:${focusVisible}) { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.ButtonText}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) inset ${SystemColors.HighlightText}; + color: ${SystemColors.HighlightText}; + fill: currentcolor; + } + + :host([disabled]), + :host([disabled]:hover), + :host([disabled]:hover) .start, + :host([disabled]:hover) .end, + :host([disabled]:hover)::slotted(svg) { + background: ${SystemColors.Canvas}; + color: ${SystemColors.GrayText}; + fill: currentcolor; + opacity: 1; + } + + :host .expanded-toggle, + :host .checkbox, + :host .radio{ + border-color: ${SystemColors.ButtonText}; + background: ${SystemColors.HighlightText}; + } + + :host([checked="true"]) .checkbox, + :host([checked="true"]) .radio { + background: ${SystemColors.HighlightText}; + border-color: ${SystemColors.HighlightText}; + } + + :host(:hover) .expanded-toggle, :host(:hover) .checkbox, :host(:hover) .radio, :host(:${focusVisible}) .expanded-toggle, @@ -369,44 +363,45 @@ export const menuItemStyles: FoundationElementTemplate< :host([checked="true"]:hover) .radio, :host([checked="true"]:${focusVisible}) .checkbox, :host([checked="true"]:${focusVisible}) .radio { - border-color: ${SystemColors.HighlightText}; - } + border-color: ${SystemColors.HighlightText}; + } - :host([aria-checked='true']) { - background: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } + :host([aria-checked="true"]) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } - :host([aria-checked='true']) .checkbox-indicator, - :host([aria-checked='true']) ::slotted([slot='checkbox-indicator']), - :host([aria-checked='true']) ::slotted([slot='radio-indicator']) { - fill: ${SystemColors.Highlight}; - } + :host([aria-checked="true"]) .checkbox-indicator, + :host([aria-checked="true"]) ::slotted([slot="checkbox-indicator"]), + :host([aria-checked="true"]) ::slotted([slot="radio-indicator"]) { + fill: ${SystemColors.Highlight}; + } - :host([aria-checked='true']) .radio-indicator { - background: ${SystemColors.Highlight}; - } + :host([aria-checked="true"]) .radio-indicator { + background: ${SystemColors.Highlight}; + } - ::slotted([slot='end']:not(svg)) { - color: ${SystemColors.ButtonText}; - } + ::slotted([slot="end"]:not(svg)) { + color: ${SystemColors.ButtonText}; + } - :host(:hover) ::slotted([slot="end"]:not(svg)), + :host(:hover) ::slotted([slot="end"]:not(svg)), :host(:${focusVisible}) ::slotted([slot="end"]:not(svg)) { - color: ${SystemColors.HighlightText}; - } - `), - - new DirectionalStyleSheetBehavior( - css` - .expand-collapse-glyph { - transform: rotate(0deg); - } - `, - css` - .expand-collapse-glyph { - transform: rotate(180deg); - } - ` - ) - ); + color: ${SystemColors.HighlightText}; + } + ` + ), + + new DirectionalStyleSheetBehavior( + css` + .expand-collapse-glyph { + transform: rotate(0deg); + } + `, + css` + .expand-collapse-glyph { + transform: rotate(180deg); + } + ` + ) + ); diff --git a/packages/components/src/menu/index.ts b/packages/components/src/menu/index.ts index f2105df1..5575dea8 100644 --- a/packages/components/src/menu/index.ts +++ b/packages/components/src/menu/index.ts @@ -1,8 +1,23 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import { + Menu as FoundationMenu, + menuTemplate as template, +} from "@microsoft/fast-foundation"; +import { fillColor, neutralLayerFloating } from "../design-tokens.js"; +import { menuStyles as styles } from "./menu.styles.js"; -import { Menu, menuTemplate as template } from '@microsoft/fast-foundation'; -import { menuStyles as styles } from '@microsoft/fast-components'; +/** + * @public + */ +export class Menu extends FoundationMenu { + /** + * @internal + */ + public connectedCallback(): void { + super.connectedCallback(); + + fillColor.setValueFor(this, neutralLayerFloating); + } +} /** * A function that returns a {@link @microsoft/fast-foundation#Menu} registration for configuring the component with a DesignSystem. @@ -11,18 +26,12 @@ import { menuStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpMenu = Menu.compose({ - baseName: 'menu', - template, - styles +export const fastMenu = Menu.compose({ + baseName: "menu", + template, + styles, }); -/** - * Base class for Menu - * @public - */ -export { Menu }; - export { styles as menuStyles }; diff --git a/packages/components/src/menu/menu.stories.ts b/packages/components/src/menu/menu.stories.ts index 36aeb7f7..b57adbc5 100644 --- a/packages/components/src/menu/menu.stories.ts +++ b/packages/components/src/menu/menu.stories.ts @@ -1,91 +1,7 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import MenuTemplate from "./fixtures/menu.html"; export default { - title: 'Components/Menu', - parameters: { - controls: { - disabled: true - }, - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - - ${getFaIcon('robot', 'start')} - Menu item 1 - ${getFaIcon('robot', 'end')} - - - Menu item 2 - - Checkbox 1 - Nested Menu item 1.1 - Nested Menu item 1.2 - Nested Menu item 1.3 - - - - ${getFaIcon('robot', 'start')} - Menu item 3 -
- Shortcut text -
-
- - Menu item 4 - - Nested Menu item 4.1 - Nested Menu item 4.2 - Nested Menu item 4.3 - - - - Menu item 5 - - Checkbox 1 - - Nested Menu item 5.1 - - Checkbox 1 - Nested Menu item 5.1.1 - Nested Menu item 5.1.2 - Nested Menu item 5.1.3 - - - - Nested Menu item 5.2 - - Nested Menu item 5.2.1 - Nested Menu item 5.2.2 - Nested Menu item 5.2.3 - - - - Nested Menu item 5.3 - - Nested Menu item 5.3.1 - Nested Menu item 5.3.2 - Nested Menu item 5.3.3 - - - - -
`; + title: "Menu", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = {}; +export const Menu = () => MenuTemplate; diff --git a/packages/components/src/menu/menu.styles.ts b/packages/components/src/menu/menu.styles.ts new file mode 100644 index 00000000..9736b9fa --- /dev/null +++ b/packages/components/src/menu/menu.styles.ts @@ -0,0 +1,59 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + display, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + controlCornerRadius, + designUnit, + fillColor, + neutralStrokeDividerRest, + strokeWidth, +} from "../design-tokens.js"; +import { elevation } from "../styles/index.js"; + +/** + * Styles for Menu + * @public + */ +export const menuStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("block")} :host { + --elevation: 11; + background: ${fillColor}; + border: calc(${strokeWidth} * 1px) solid transparent; + ${elevation} + margin: 0; + border-radius: calc(${controlCornerRadius} * 1px); + padding: calc(${designUnit} * 1px) 0; + max-width: 368px; + min-width: 64px; + } + + :host([slot="submenu"]) { + width: max-content; + margin: 0 calc(${designUnit} * 1px); + } + + ::slotted(hr) { + box-sizing: content-box; + height: 0; + margin: 0; + border: none; + border-top: calc(${strokeWidth} * 1px) solid ${neutralStrokeDividerRest}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + background: ${SystemColors.Canvas}; + border-color: ${SystemColors.CanvasText}; + } + ` + ) + ); diff --git a/packages/components/src/number-field/index.ts b/packages/components/src/number-field/index.ts index c733205d..5d04138b 100644 --- a/packages/components/src/number-field/index.ts +++ b/packages/components/src/number-field/index.ts @@ -1,16 +1,31 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - +import { attr } from "@microsoft/fast-element"; import { - NumberField as FoundationNumberField, - NumberFieldOptions, - numberFieldTemplate as template -} from '@microsoft/fast-foundation'; -import { NumberField } from '@microsoft/fast-components'; -import { numberFieldStyles as styles } from './number-field.styles'; + NumberField as FoundationNumberField, + NumberFieldOptions, + numberFieldTemplate as template, +} from "@microsoft/fast-foundation"; +import { numberFieldStyles as styles } from "./number-field.styles.js"; + +/** + * Number field appearances + * @public + */ +export type NumberFieldAppearance = "filled" | "outline"; -// TODO -// we need to add error/invalid +/** + * @internal + */ +export class NumberField extends FoundationNumberField { + /** + * The appearance of the element. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: NumberFieldAppearance = "outline"; +} /** * A function that returns a {@link @microsoft/fast-foundation#NumberField} registration for configuring the component with a DesignSystem. @@ -19,30 +34,24 @@ import { numberFieldStyles as styles } from './number-field.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus} */ -export const jpNumberField = NumberField.compose({ - baseName: 'number-field', - baseClass: FoundationNumberField, - styles, - template, - shadowOptions: { - delegatesFocus: true - }, - stepDownGlyph: /* html */ ` +export const fastNumberField = NumberField.compose({ + baseName: "number-field", + baseClass: FoundationNumberField, + styles, + template, + shadowOptions: { + delegatesFocus: true, + }, + stepDownGlyph: /* html */ ` `, - stepUpGlyph: /* html */ ` + stepUpGlyph: /* html */ ` - ` + `, }); -export { NumberField, NumberFieldAppearance } from '@microsoft/fast-components'; - -/** - * Styles for NumberField - * @public - */ export { styles as numberFieldStyles }; diff --git a/packages/components/src/number-field/number-field.stories.ts b/packages/components/src/number-field/number-field.stories.ts index 8db483d8..426d1379 100644 --- a/packages/components/src/number-field/number-field.stories.ts +++ b/packages/components/src/number-field/number-field.stories.ts @@ -1,133 +1,24 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { getFaIcon, setTheme } from '../utilities/storybook'; -import { NumberField } from './index'; - -export default { - title: 'Components/Number Field', - argTypes: { - label: { control: 'text' }, - placeholder: { control: 'text' }, - value: { control: 'number' }, - maxLength: { control: 'number' }, - size: { control: 'number' }, - isReadOnly: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isAutoFocused: { control: 'boolean' }, - startIcon: { control: 'boolean' }, - endIcon: { control: 'boolean' }, - appearance: { control: 'radio', options: ['outline', 'filled'] }, - onChange: { - action: 'changed', - table: { - disable: true - } +import addons from "@storybook/addons"; +import { STORY_RENDERED } from "@storybook/core-events"; +import NumberFieldTemplate from "./fixtures/number-field.html"; +import "./index.js"; + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("number-field")) { + document.querySelectorAll(".form").forEach(el => { + if (el instanceof HTMLFormElement) { + el.onsubmit = event => { + event.preventDefault(); + const form: HTMLFormElement = document.forms["myForm"]; + console.log(form.elements["fname"].value, "value of input"); + }; + } + }); } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.startIcon ? getFaIcon('search', 'start') : ''} - ${args.label} - ${args.endIcon ? getFaIcon('euro-sign', 'end') : ''} - ` - ); - - const numberField = container.firstChild as NumberField; - - if (args.value) { - numberField.value = args.value; - } - - if (args.onChange) { - numberField.addEventListener('change', args.onChange); - } - - return numberField; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Number Field Label', - placeholder: '', - value: '', - maxLength: '', - size: '', - isReadOnly: false, - isDisabled: false, - isAutoFocused: false, - startIcon: false, - endIcon: false, - appearance: 'outline', - onChange: action('number-field-onchange') -}; - -export const WithPlaceholder: StoryObj = { render: Template.bind({}) }; -WithPlaceholder.args = { - ...Default.args, - placeholder: 'Placeholder Text' -}; +}); -export const WithAutofocus: StoryObj = { render: Template.bind({}) }; -WithAutofocus.args = { - ...Default.args, - autofocus: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - disabled: true -}; - -export const WithSize: StoryObj = { render: Template.bind({}) }; -WithSize.args = { - ...Default.args, - placeholder: 'This number field is 50 characters in width', - size: 50 -}; - -export const WithMaxLength: StoryObj = { render: Template.bind({}) }; -WithMaxLength.args = { - ...Default.args, - placeholder: 'This number field can only contain a maximum of 10 characters', - maxLength: 10 -}; - -export const WithReadonly: StoryObj = { render: Template.bind({}) }; -WithReadonly.args = { - ...Default.args, - readonly: true -}; - -export const WithStartIcon: StoryObj = { render: Template.bind({}) }; -WithStartIcon.args = { - ...Default.args, - startIcon: true +export default { + title: "Number Field", }; -export const WithEndIcon: StoryObj = { render: Template.bind({}) }; -WithEndIcon.args = { - ...Default.args, - endIcon: true -}; +export const NumberField = () => NumberFieldTemplate; diff --git a/packages/components/src/number-field/number-field.styles.ts b/packages/components/src/number-field/number-field.styles.ts index 31e8db4a..a765a2e6 100644 --- a/packages/components/src/number-field/number-field.styles.ts +++ b/packages/components/src/number-field/number-field.styles.ts @@ -1,53 +1,229 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + NumberFieldOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - FoundationElementTemplate, - NumberFieldOptions -} from '@microsoft/fast-foundation'; -import { neutralForegroundRest } from '../design-tokens'; -import { BaseFieldStyles } from '../styles/index'; + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeOuter, + focusStrokeWidth, + neutralFillHover, + neutralFillInputHover, + neutralFillInputRest, + neutralFillRest, + neutralForegroundRest, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Number Field * @public */ export const numberFieldStyles: FoundationElementTemplate< - ElementStyles, - NumberFieldOptions -> = (context, definition) => css` - ${BaseFieldStyles} - - .controls { - opacity: 0; - } - - .step-up-glyph, - .step-down-glyph { - display: block; - padding: 4px 10px; - cursor: pointer; - } - - .step-up-glyph:before, - .step-down-glyph:before { - content: ''; - display: block; - border: solid transparent 6px; - } - - .step-up-glyph:before { - border-bottom-color: ${neutralForegroundRest}; - } - - .step-down-glyph:before { - border-top-color: ${neutralForegroundRest}; - } - - :host(:hover:not([disabled])) .controls, - :host(:focus-within:not([disabled])) .controls { - opacity: 1; - } -`; + ElementStyles, + NumberFieldOptions +> = (context, definition) => + css` + ${display("inline-block")} :host { + font-family: ${bodyFont}; + outline: none; + user-select: none; + } + + .root { + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: row; + color: ${neutralForegroundRest}; + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + height: calc(${heightNumber} * 1px); + align-items: baseline; + } + + .control { + -webkit-appearance: none; + font: inherit; + background: transparent; + border: 0; + color: inherit; + height: calc(100% - 4px); + width: 100%; + margin-top: auto; + margin-bottom: auto; + border: none; + padding: 0 calc(${designUnit} * 2px + 1px); + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + } + + .control:hover, + .control:${focusVisible}, + .control:disabled, + .control:active { + outline: none; + } + + .controls { + opacity: 0; + } + + .label { + display: block; + color: ${neutralForegroundRest}; + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + margin-bottom: 4px; + } + + .label__hidden { + display: none; + visibility: hidden; + } + + .start, + .control, + .controls, + .end { + align-self: center; + } + + .start, + .end { + margin: auto; + fill: currentcolor; + } + + .step-up-glyph, + .step-down-glyph { + display: block; + padding: 4px 10px; + cursor: pointer; + } + + .step-up-glyph:before, + .step-down-glyph:before { + content: ''; + display: block; + border: solid transparent 6px; + } + + .step-up-glyph:before { + border-bottom-color: ${neutralForegroundRest}; + } + + .step-down-glyph:before { + border-top-color: ${neutralForegroundRest}; + } + + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + } + + .start { + margin-inline-start: 11px; + } + + .end { + margin-inline-end: 11px; + } + + :host(:hover:not([disabled])) .root { + background: ${neutralFillInputHover}; + border-color: ${accentFillHover}; + } + + :host(:active:not([disabled])) .root { + background: ${neutralFillInputHover}; + border-color: ${accentFillActive}; + } + + :host(:focus-within:not([disabled])) .root { + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${focusStrokeOuter} inset; + } + + :host(:hover:not([disabled])) .controls, + :host(:focus-within:not([disabled])) .controls { + opacity: 1; + } + + :host([appearance="filled"]) .root { + background: ${neutralFillRest}; + } + + :host([appearance="filled"]:hover:not([disabled])) .root { + background: ${neutralFillHover}; + } + + :host([disabled]) .label, + :host([readonly]) .label, + :host([readonly]) .control, + :host([disabled]) .control { + cursor: ${disabledCursor}; + } + + :host([disabled]) { + opacity: ${disabledOpacity}; + } + + :host([disabled]) .control { + border-color: ${neutralStrokeRest}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .root, + :host([appearance="filled"]) .root { + forced-color-adjust: none; + background: ${SystemColors.Field}; + border-color: ${SystemColors.FieldText}; + } + :host(:hover:not([disabled])) .root, + :host([appearance="filled"]:hover:not([disabled])) .root, + :host([appearance="filled"]:hover) .root { + background: ${SystemColors.Field}; + border-color: ${SystemColors.Highlight}; + } + .start, + .end { + fill: currentcolor; + } + :host([disabled]) { + opacity: 1; + } + :host([disabled]) .root, + :host([appearance="filled"]:hover[disabled]) .root { + border-color: ${SystemColors.GrayText}; + background: ${SystemColors.Field}; + } + :host(:focus-within:enabled) .root { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 1px ${SystemColors.Highlight} inset; + } + input::placeholder { + color: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/option/index.ts b/packages/components/src/option/index.ts index dfebd945..73890b45 100644 --- a/packages/components/src/option/index.ts +++ b/packages/components/src/option/index.ts @@ -1,32 +1,29 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - import { ListboxOption, - listboxOptionTemplate as template -} from '@microsoft/fast-foundation'; -import { optionStyles as styles } from './option.styles'; + listboxOptionTemplate as template, +} from "@microsoft/fast-foundation"; +import { optionStyles as styles } from "./listbox-option.styles.js"; /** - * A function that returns a Option registration for configuring the component with a DesignSystem. - * - * - * @public - * @remarks - * Generates HTML Element: `` - * - */ -export const jpOption = ListboxOption.compose({ - baseName: 'option', +* A function that returns a {@link @microsoft/fast-foundation#ListboxOption} registration for configuring the component with a DesignSystem. +* Implements {@link @microsoft/fast-foundation#listboxOptionTemplate} +* +* +* @public +* @remarks +* Generates HTML Element: `` +* +*/ +export const fastOption = ListboxOption.compose({ + baseName: "option", template, - styles + styles, }); /** - * Base class for Option - * @public - */ -export { ListboxOption as Option }; +* Base class for ListboxOption +* @public +*/ +export { ListboxOption }; export { styles as optionStyles }; diff --git a/packages/components/src/option/option.stories.ts b/packages/components/src/option/option.stories.ts index 3edc6f6a..085ea1bb 100644 --- a/packages/components/src/option/option.stories.ts +++ b/packages/components/src/option/option.stories.ts @@ -1,52 +1,30 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import { addons } from "@storybook/addons"; +import { STORY_RENDERED } from "@storybook/core-events"; +import Examples from "./fixtures/base.html"; +import type { ListboxOption as ListboxOptionType } from "./index.js"; -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("listbox-option")) { + const checkedOption = document.getElementById( + "checked-option" + ) as ListboxOptionType; + checkedOption.checked = true; -export default { - title: 'Components/Option', - argTypes: { - label: { control: 'text' }, - isDisabled: { control: 'boolean' }, - isSelected: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - return ` - ${args.label} - `; -}; + const selectedOption = document.getElementById( + "selected-option" + ) as ListboxOptionType; + selectedOption.selected = true; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Option Label', - isDisabled: false, - isSelected: false -}; + const checkedSelectedOption = document.getElementById( + "checked-selected-option" + ) as ListboxOptionType; + checkedSelectedOption.selected = true; + checkedSelectedOption.checked = true; + } +}); -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true +export default { + title: "Listbox Option", }; -export const WithSelected: StoryObj = { render: Template.bind({}) }; -WithSelected.args = { - ...Default.args, - isSelected: true -}; +export const ListboxOption = () => Examples; diff --git a/packages/components/src/option/option.styles.ts b/packages/components/src/option/option.styles.ts index f4cfc249..4322edfe 100644 --- a/packages/components/src/option/option.styles.ts +++ b/packages/components/src/option/option.styles.ts @@ -1,154 +1,174 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - +import { css } from "@microsoft/fast-element"; +import type { ElementStyles } from "@microsoft/fast-element"; import { - accentFillActive, - accentFillFocus, - accentFillHover, - accentFillRest, - bodyFont, - controlCornerRadius, - designUnit, - disabledOpacity, - focusStrokeWidth, - foregroundOnAccentActive, - foregroundOnAccentFocus, - foregroundOnAccentHover, - foregroundOnAccentRest, - neutralFillHover, - neutralForegroundRest, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '@microsoft/fast-components'; -import type { ElementStyles } from '@microsoft/fast-element'; -import { css } from '@microsoft/fast-element'; + disabledCursor, + display, + forcedColorsStylesheetBehavior, +} from "@microsoft/fast-foundation"; import type { - FoundationElementTemplate, - ListboxOptionOptions -} from '@microsoft/fast-foundation'; + FoundationElementTemplate, + ListboxOptionOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; -import { heightNumber } from '../styles'; + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeInner, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillStealthActive, + neutralFillStealthHover, + neutralFillStealthRest, + neutralForegroundRest, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/size.js"; /** - * Styles for Option + * Styles for the {@link @microsoft/fast-components#fastOption | Listbox Option} component. + * + * @param context - the element definition context + * @param definition - the foundation element definition + * @returns The element styles for the listbox option component + * * @public */ export const optionStyles: FoundationElementTemplate< - ElementStyles, - ListboxOptionOptions + ElementStyles, + ListboxOptionOptions > = (context, definition) => - css` - ${display('inline-flex')} :host { - align-items: center; - font-family: ${bodyFont}; - border-radius: calc(${controlCornerRadius} * 1px); - border: calc(${focusStrokeWidth} * 1px) solid transparent; - box-sizing: border-box; - color: ${neutralForegroundRest}; - cursor: pointer; - flex: 0 0 auto; - fill: currentcolor; - font-size: ${typeRampBaseFontSize}; - height: calc(${heightNumber} * 1px); - line-height: ${typeRampBaseLineHeight}; - margin: 0 calc(${designUnit} * 1px); - outline: none; - overflow: hidden; - padding: 0 calc(${designUnit} * 2.25px); - user-select: none; - white-space: nowrap; - } - - /* TODO should we use outline instead of background for focus to support multi-selection */ - :host(:${focusVisible}) { - background: ${accentFillFocus}; - color: ${foregroundOnAccentFocus}; - } - - :host([aria-selected='true']) { - background: ${accentFillRest}; - color: ${foregroundOnAccentRest}; - } - - :host(:hover) { - background: ${accentFillHover}; - color: ${foregroundOnAccentHover}; - } - - :host(:active) { - background: ${accentFillActive}; - color: ${foregroundOnAccentActive}; - } - - :host(:not([aria-selected='true']):hover), - :host(:not([aria-selected='true']):active) { - background: ${neutralFillHover}; - color: ${neutralForegroundRest}; - } - - :host([disabled]) { - cursor: ${disabledCursor}; - opacity: ${disabledOpacity}; - } - - :host([disabled]:hover) { - background-color: inherit; - } - - .content { - grid-column-start: 2; - justify-self: start; - overflow: hidden; - text-overflow: ellipsis; - } - - .start, - .end, - ::slotted(svg) { - display: flex; - } - - ::slotted(svg) { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - height: calc(${designUnit} * 4px); - width: calc(${designUnit} * 4px); - } - - ::slotted([slot='end']) { - margin-inline-start: 1ch; - } - - ::slotted([slot='start']) { - margin-inline-end: 1ch; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host { - border-color: transparent; - forced-color-adjust: none; - color: ${SystemColors.ButtonText}; - fill: currentcolor; - } - - :host(:not([aria-selected='true']):hover), - :host([aria-selected='true']) { - background: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - } - - :host([disabled]), - :host([disabled]:not([aria-selected='true']):hover) { - background: ${SystemColors.Canvas}; - color: ${SystemColors.GrayText}; - fill: currentcolor; - opacity: 1; - } - `) - ); + css` + ${display("inline-flex")} :host { + align-items: center; + font-family: ${bodyFont}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${focusStrokeWidth} * 1px) solid transparent; + box-sizing: border-box; + background: ${neutralFillStealthRest}; + color: ${neutralForegroundRest}; + cursor: pointer; + flex: 0 0 auto; + fill: currentcolor; + font-size: ${typeRampBaseFontSize}; + height: calc(${heightNumber} * 1px); + line-height: ${typeRampBaseLineHeight}; + margin: 0 calc((${designUnit} - ${focusStrokeWidth}) * 1px); + outline: none; + overflow: hidden; + padding: 0 1ch; + user-select: none; + white-space: nowrap; + } + + :host(:not([disabled]):not([aria-selected="true"]):hover) { + background: ${neutralFillStealthHover}; + } + + :host(:not([disabled]):not([aria-selected="true"]):active) { + background: ${neutralFillStealthActive}; + } + + :host([aria-selected="true"]) { + background: ${accentFillRest}; + color: ${foregroundOnAccentRest}; + } + + :host(:not([disabled])[aria-selected="true"]:hover) { + background: ${accentFillHover}; + color: ${foregroundOnAccentHover}; + } + + :host(:not([disabled])[aria-selected="true"]:active) { + background: ${accentFillActive}; + color: ${foregroundOnAccentActive}; + } + + :host([disabled]) { + cursor: ${disabledCursor}; + opacity: ${disabledOpacity}; + } + + .content { + grid-column-start: 2; + justify-self: start; + overflow: hidden; + text-overflow: ellipsis; + } + + .start, + .end, + ::slotted(svg) { + display: flex; + } + + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + height: calc(${designUnit} * 4px); + width: calc(${designUnit} * 4px); + } + + ::slotted([slot="end"]) { + margin-inline-start: 1ch; + } + + ::slotted([slot="start"]) { + margin-inline-end: 1ch; + } + + :host([aria-checked="true"][aria-selected="false"]) { + border-color: ${focusStrokeOuter}; + } + + :host([aria-checked="true"][aria-selected="true"]) { + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 2 * 1px) inset + ${focusStrokeInner}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + border-color: transparent; + forced-color-adjust: none; + color: ${SystemColors.ButtonText}; + fill: currentcolor; + } + + :host(:not([aria-selected="true"]):hover), + :host([aria-selected="true"]) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + :host([disabled]), + :host([disabled][aria-selected="false"]:hover) { + background: ${SystemColors.Canvas}; + color: ${SystemColors.GrayText}; + fill: currentcolor; + opacity: 1; + } + + :host([aria-checked="true"][aria-selected="false"]) { + background: ${SystemColors.ButtonFace}; + color: ${SystemColors.ButtonText}; + border-color: ${SystemColors.ButtonText}; + } + + :host([aria-checked="true"][aria-selected="true"]), + :host([aria-checked="true"][aria-selected="true"]:hover) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + border-color: ${SystemColors.ButtonText}; + } + ` + ) + ); diff --git a/packages/components/src/picker/README.md b/packages/components/src/picker/README.md new file mode 100644 index 00000000..9e908d5f --- /dev/null +++ b/packages/components/src/picker/README.md @@ -0,0 +1 @@ +# fast-picker \ No newline at end of file diff --git a/packages/components/src/picker/fixtures/picker.html b/packages/components/src/picker/fixtures/picker.html new file mode 100644 index 00000000..f289faf4 --- /dev/null +++ b/packages/components/src/picker/fixtures/picker.html @@ -0,0 +1,162 @@ + + +

Picker

+ +

Default

+ + +

Filter query and filter selection off

+ + +

Preselection

+ + +

Custom menu

+ + + + pre-option + +
Group 1
+ + +
Group 2
+ + + + post-option + +
+
+ +

Custom menu no options

+ + + + pre-option + +
Group 1
+ + +
Group 2
+ + + + post-option + +
+
+ +

Single item

+ + +

Multiple items, limit to 3

+ + +

Custom content templates

+ + +

Menu above

+ + +

Menu above or below

+ + +

Menu above or below, not scaling

+ + +
diff --git a/packages/components/src/picker/index.ts b/packages/components/src/picker/index.ts new file mode 100644 index 00000000..34e1d33f --- /dev/null +++ b/packages/components/src/picker/index.ts @@ -0,0 +1,114 @@ +import { + FoundationElementDefinition, + PickerMenu as FoundationPickerMenu, + Picker, + PickerList, + PickerListItem, + pickerListItemTemplate, + pickerListTemplate, + PickerMenuOption, + pickerMenuOptionTemplate, + pickerMenuTemplate, + pickerTemplate, +} from "@microsoft/fast-foundation"; +import { fillColor, neutralLayerFloating } from "../design-tokens.js"; +import { pickerStyles } from "./picker.styles.js"; +import { pickerMenuStyles } from "./picker-menu.styles.js"; +import { pickerMenuOptionStyles } from "./picker-menu-option.styles.js"; +import { pickerListStyles } from "./picker-list.styles.js"; +import { pickerListItemStyles } from "./picker-list-item.styles.js"; + +/** + * The FAST Picker Custom Element. Implements {@link @microsoft/fast-foundation#Picker}, + * {@link @microsoft/fast-foundation#PickerTemplate} + * + * + * @alpha + * @remarks + * * Generates HTML Element: `` + */ +export const fastPicker = Picker.compose({ + baseName: "picker", + template: pickerTemplate, + styles: pickerStyles, + shadowOptions: {}, +}); + +/** + * Base class for Picker + * @alpha + */ +export { Picker }; + +/** + * @public + */ +export class PickerMenu extends FoundationPickerMenu { + /** + * @public + */ + public connectedCallback(): void { + fillColor.setValueFor(this, neutralLayerFloating); + + super.connectedCallback(); + } +} + +/** + * Component that displays the list of available picker options + * + * + * @alpha + * @remarks + * HTML Element: \ + */ +export const fastPickerMenu = PickerMenu.compose({ + baseName: "picker-menu", + baseClass: FoundationPickerMenu, + template: pickerMenuTemplate, + styles: pickerMenuStyles, +}); + +/** + * Component that displays available picker menu options + * + * + * @alpha + * @remarks + * HTML Element: \ + */ +export const fastPickerMenuOption = PickerMenuOption.compose({ + baseName: "picker-menu-option", + template: pickerMenuOptionTemplate, + styles: pickerMenuOptionStyles, +}); + +/** + * Component that displays the list of selected picker items along + * with the input combobox + * + * @alpha + * @remarks + * HTML Element: \ + * + */ +export const fastPickerList = PickerList.compose({ + baseName: "picker-list", + template: pickerListTemplate, + styles: pickerListStyles, +}); + +/** + * Component that displays selected items + * + * @alpha + * @remarks + * HTML Element: \ + */ +export const fastPickerListItem = PickerListItem.compose({ + baseName: "picker-list-item", + template: pickerListItemTemplate, + styles: pickerListItemStyles, +}); + +export { pickerStyles, pickerListItemStyles, pickerMenuOptionStyles, pickerMenuStyles }; diff --git a/packages/components/src/picker/picker-list-item.open-ui.definition.ts b/packages/components/src/picker/picker-list-item.open-ui.definition.ts new file mode 100644 index 00000000..be3bdfa1 --- /dev/null +++ b/packages/components/src/picker/picker-list-item.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Picker list item", + url: "https://fast.design/docs/components/picker-list-item", +}; diff --git a/packages/components/src/picker/picker-list-item.styles.ts b/packages/components/src/picker/picker-list-item.styles.ts new file mode 100644 index 00000000..e2b82c37 --- /dev/null +++ b/packages/components/src/picker/picker-list-item.styles.ts @@ -0,0 +1,99 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentActive, + neutralFillStealthActive, + neutralFillStealthFocus, + neutralFillStealthHover, + neutralFillStealthRest, + neutralForegroundRest, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +/** + * Styles for Picker list item + * @public + */ +export const pickerListItemStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + :host { + display: flex; + align-items: center; + justify-items: center; + font-family: ${bodyFont}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${focusStrokeWidth} * 1px) solid transparent; + box-sizing: border-box; + background: ${neutralFillStealthRest}; + color: ${neutralForegroundRest}; + cursor: pointer; + fill: currentcolor; + font-size: ${typeRampBaseFontSize}; + height: calc(${heightNumber} * 1px); + line-height: ${typeRampBaseLineHeight}; + outline: none; + overflow: hidden; + padding: 0 calc(${designUnit} * 2.25px); + user-select: none; + white-space: nowrap; + } + + :host(:hover) { + background: ${neutralFillStealthHover}; + } + + :host(:active) { + background: ${neutralFillStealthActive}; + } + + :host(:${focusVisible}) { + background: ${neutralFillStealthFocus}; + border-color: ${focusStrokeOuter}; + } + + :host([aria-selected="true"]) { + background: ${accentFillRest}; + color: ${foregroundOnAccentActive}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + border-color: transparent; + forced-color-adjust: none; + color: ${SystemColors.ButtonText}; + fill: currentcolor; + } + + :host(:not([aria-selected="true"]):hover), + :host([aria-selected="true"]) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + :host([disabled]), + :host([disabled]:not([aria-selected="true"]):hover) { + background: ${SystemColors.Canvas}; + color: ${SystemColors.GrayText}; + fill: currentcolor; + opacity: 1; + } + ` + ) + ); diff --git a/packages/components/src/picker/picker-list-item.vscode.definition.ts b/packages/components/src/picker/picker-list-item.vscode.definition.ts new file mode 100644 index 00000000..a978e14e --- /dev/null +++ b/packages/components/src/picker/picker-list-item.vscode.definition.ts @@ -0,0 +1,25 @@ +export default { + version: 1.1, + tags: [ + { + name: "fast-picker-list-item", + title: "Picker list item", + description: "The FAST picker-list-item element", + attributes: [ + { + name: "value", + description: "The value attribute", + default: "", + required: true, + type: "string", + }, + ], + slots: [ + { + name: "", + description: "The default slot", + }, + ], + }, + ], +}; diff --git a/packages/components/src/picker/picker-list.open-ui.definition.ts b/packages/components/src/picker/picker-list.open-ui.definition.ts new file mode 100644 index 00000000..a77b76a9 --- /dev/null +++ b/packages/components/src/picker/picker-list.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Picker list", + url: "https://fast.design/docs/components/picker-list", +}; diff --git a/packages/components/src/picker/picker-list.styles.ts b/packages/components/src/picker/picker-list.styles.ts new file mode 100644 index 00000000..d4854abb --- /dev/null +++ b/packages/components/src/picker/picker-list.styles.ts @@ -0,0 +1,82 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + accentFillActive, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + focusStrokeOuter, + neutralFillInputHover, + neutralFillInputRest, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +/** + * Styles for Picker list + * @public + */ +export const pickerListStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + :host { + display: flex; + flex-direction: row; + column-gap: calc(${designUnit} * 1px); + row-gap: calc(${designUnit} * 1px); + flex-wrap: wrap; + } + + ::slotted([role="combobox"]) { + min-width: 260px; + width: auto; + box-sizing: border-box; + color: ${neutralForegroundRest}; + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + height: calc(${heightNumber} * 1px); + font-family: ${bodyFont}; + outline: none; + user-select: none; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + padding: 0 calc(${designUnit} * 2px + 1px); + } + + ::slotted([role="combobox"]:active) { { + background: ${neutralFillInputHover}; + border-color: ${accentFillActive}; + } + + ::slotted([role="combobox"]:focus-within) { + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 1px ${focusStrokeOuter} inset; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + ::slotted([role="combobox"]:active) { + background: ${SystemColors.Field}; + border-color: ${SystemColors.Highlight}; + } + ::slotted([role="combobox"]:focus-within) { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 1px ${SystemColors.Highlight} inset; + } + ::slotted(input:placeholder) { + color: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/picker/picker-list.vscode.definition.ts b/packages/components/src/picker/picker-list.vscode.definition.ts new file mode 100644 index 00000000..26bca121 --- /dev/null +++ b/packages/components/src/picker/picker-list.vscode.definition.ts @@ -0,0 +1,32 @@ +export default { + version: 1.1, + tags: [ + { + name: "fast-picker-list", + title: "Picker list", + description: "The FAST picker-list element", + attributes: [ + { + name: "label", + description: "The label attribute", + type: "string", + default: "", + required: false, + }, + { + name: "labelledby", + description: "The labelledby attribute", + type: "string", + default: "", + required: false, + }, + ], + slots: [ + { + name: "", + description: "The default slot", + }, + ], + }, + ], +}; diff --git a/packages/components/src/picker/picker-menu-option.open-ui.definition.ts b/packages/components/src/picker/picker-menu-option.open-ui.definition.ts new file mode 100644 index 00000000..6d754586 --- /dev/null +++ b/packages/components/src/picker/picker-menu-option.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Picker menu item", + url: "https://fast.design/docs/components/picker-menu-option", +}; diff --git a/packages/components/src/picker/picker-menu-option.styles.ts b/packages/components/src/picker/picker-menu-option.styles.ts new file mode 100644 index 00000000..6916cc8c --- /dev/null +++ b/packages/components/src/picker/picker-menu-option.styles.ts @@ -0,0 +1,114 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillStealthActive, + neutralFillStealthFocus, + neutralFillStealthHover, + neutralFillStealthRest, + neutralForegroundRest, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +/** + * Styles for Picker menu option + * @public + */ +export const pickerMenuOptionStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + :host { + display: flex; + align-items: center; + justify-items: center; + font-family: ${bodyFont}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${focusStrokeWidth} * 1px) solid transparent; + box-sizing: border-box; + background: ${neutralFillStealthRest}; + color: ${neutralForegroundRest}; + cursor: pointer; + fill: currentcolor; + font-size: ${typeRampBaseFontSize}; + min-height: calc(${heightNumber} * 1px); + line-height: ${typeRampBaseLineHeight}; + margin: 0 calc(${designUnit} * 1px); + outline: none; + overflow: hidden; + padding: 0 calc(${designUnit} * 2.25px); + user-select: none; + white-space: nowrap; + } + + :host(:${focusVisible}[role="listitem"]) { + border-color: ${focusStrokeOuter}; + background: ${neutralFillStealthFocus}; + } + + :host(:hover) { + background: ${neutralFillStealthHover}; + } + + :host(:active) { + background: ${neutralFillStealthActive}; + } + + :host([aria-selected="true"]) { + background: ${accentFillRest}; + color: ${foregroundOnAccentRest}; + } + + :host([aria-selected="true"]:hover) { + background: ${accentFillHover}; + color: ${foregroundOnAccentHover}; + } + + :host([aria-selected="true"]:active) { + background: ${accentFillActive}; + color: ${foregroundOnAccentActive}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + border-color: transparent; + forced-color-adjust: none; + color: ${SystemColors.ButtonText}; + fill: currentcolor; + } + + :host(:not([aria-selected="true"]):hover), + :host([aria-selected="true"]) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + :host([disabled]), + :host([disabled]:not([aria-selected="true"]):hover) { + background: ${SystemColors.Canvas}; + color: ${SystemColors.GrayText}; + fill: currentcolor; + opacity: 1; + } + ` + ) + ); diff --git a/packages/components/src/picker/picker-menu-option.vscode.definition.ts b/packages/components/src/picker/picker-menu-option.vscode.definition.ts new file mode 100644 index 00000000..b42beefd --- /dev/null +++ b/packages/components/src/picker/picker-menu-option.vscode.definition.ts @@ -0,0 +1,25 @@ +export default { + version: 1.1, + tags: [ + { + name: "fast-picker-menu-option", + title: "Picker menu item", + description: "The FAST picker-menu-option element", + attributes: [ + { + name: "value", + description: "The value attribute", + default: "", + required: true, + type: "string", + }, + ], + slots: [ + { + name: "", + description: "The default slot", + }, + ], + }, + ], +}; diff --git a/packages/components/src/picker/picker-menu.open-ui.definition.ts b/packages/components/src/picker/picker-menu.open-ui.definition.ts new file mode 100644 index 00000000..e8fc5d08 --- /dev/null +++ b/packages/components/src/picker/picker-menu.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Picker menu", + url: "https://fast.design/docs/components/picker-menu", +}; diff --git a/packages/components/src/picker/picker-menu.styles.ts b/packages/components/src/picker/picker-menu.styles.ts new file mode 100644 index 00000000..f0a3bb27 --- /dev/null +++ b/packages/components/src/picker/picker-menu.styles.ts @@ -0,0 +1,59 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + controlCornerRadius, + designUnit, + fillColor, + strokeWidth, +} from "../design-tokens.js"; +import { elevation } from "../styles/index.js"; + +/** + * Styles for Picker menu + * @public + */ +export const pickerMenuStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + :host { + background: ${fillColor}; + --elevation: 11; + /* TODO: a mechanism to manage z-index across components + https://github.com/microsoft/fast/issues/3813 */ + z-index: 1000; + display: flex; + width: 100%; + max-height: 100%; + min-height: 58px; + box-sizing: border-box; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + pointer-events: auto; + border-radius: calc(${controlCornerRadius} * 1px); + padding: calc(${designUnit} * 1px) 0; + border: calc(${strokeWidth} * 1px) solid transparent; + ${elevation} + } + + .suggestions-available-alert { + height: 0; + opacity: 0; + overflow: hidden; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + background: ${SystemColors.Canvas}; + border-color: ${SystemColors.CanvasText}; + } + ` + ) + ); diff --git a/packages/components/src/picker/picker-menu.vscode.definition.ts b/packages/components/src/picker/picker-menu.vscode.definition.ts new file mode 100644 index 00000000..2345fa21 --- /dev/null +++ b/packages/components/src/picker/picker-menu.vscode.definition.ts @@ -0,0 +1,25 @@ +export default { + version: 1.1, + tags: [ + { + name: "fast-picker-menu", + title: "Picker menu", + description: "The FAST picker-menu element", + attributes: [], + slots: [ + { + name: "", + description: "The default slot", + }, + { + name: "header-region", + description: "The header-region slot", + }, + { + name: "footer-region", + description: "The footer-region slot", + }, + ], + }, + ], +}; diff --git a/packages/components/src/picker/picker.open-ui.definition.ts b/packages/components/src/picker/picker.open-ui.definition.ts new file mode 100644 index 00000000..1fef29ce --- /dev/null +++ b/packages/components/src/picker/picker.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Picker", + url: "https://fast.design/docs/components/picker", +}; diff --git a/packages/components/src/picker/picker.stories.ts b/packages/components/src/picker/picker.stories.ts new file mode 100644 index 00000000..bef91a15 --- /dev/null +++ b/packages/components/src/picker/picker.stories.ts @@ -0,0 +1,33 @@ +import { html, ViewTemplate } from "@microsoft/fast-element"; +import addons from "@storybook/addons"; +import { STORY_RENDERED } from "@storybook/core-events"; +import { Picker } from "@microsoft/fast-foundation"; +import PickerTemplate from "./fixtures/picker.html"; + +const optionContentsTemplate: ViewTemplate = html` +
+ ${x => x.value} +
+`; + +const itemContentsTemplate: ViewTemplate = html` +
+ ${x => x.value} +
+`; + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("picker")) { + const customTemplatePicker = document.getElementById( + "customtemplatepicker" + ) as Picker; + customTemplatePicker.menuOptionContentsTemplate = optionContentsTemplate; + customTemplatePicker.listItemContentsTemplate = itemContentsTemplate; + } +}); + +export default { + title: "Picker", +}; + +export const picker = () => PickerTemplate; diff --git a/packages/components/src/picker/picker.styles.ts b/packages/components/src/picker/picker.styles.ts new file mode 100644 index 00000000..438eed23 --- /dev/null +++ b/packages/components/src/picker/picker.styles.ts @@ -0,0 +1,57 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { + bodyFont, + designUnit, + fillColor, + typeRampBaseFontSize, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +/** + * Styles for Picker + * @public + */ +export const pickerStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + .region { + z-index: 1000; + overflow: hidden; + display: flex; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + } + + .loaded { + opacity: 1; + pointer-events: none; + } + + .loading-display, + .no-options-display { + background: ${fillColor}; + width: 100%; + min-height: calc(${heightNumber} * 1px); + display: flex; + flex-direction: column; + align-items: center; + justify-items: center; + padding: calc(${designUnit} * 1px); + } + + .loading-progress { + width: 42px; + height: 42px; + } + + .bottom { + flex-direction: column; + } + + .top { + flex-direction: column-reverse; + } + `; diff --git a/packages/components/src/picker/picker.vscode.definition.ts b/packages/components/src/picker/picker.vscode.definition.ts new file mode 100644 index 00000000..6ab60359 --- /dev/null +++ b/packages/components/src/picker/picker.vscode.definition.ts @@ -0,0 +1,114 @@ +export default { + version: 1.1, + tags: [ + { + name: "fast-picker", + title: "Picker", + description: "The FAST picker element", + attributes: [ + { + name: "selection", + description: "The selection attribute", + default: "", + required: false, + type: "string", + }, + { + name: "options", + description: "The options attribute", + required: false, + type: "string", + default: "", + }, + { + name: "filter-selected", + description: "The filter-selected attribute", + type: "boolean", + default: true, + required: false, + }, + { + name: "filter-query", + description: "The filter-query attribute", + type: "boolean", + default: true, + required: false, + }, + { + name: "max-selected", + description: "The max-selected attribute", + type: "number", + default: "", + required: false, + }, + { + name: "no-suggestions-text", + description: "The no-suggestions-text attribute", + type: "string", + default: "", + required: false, + }, + { + name: "suggestions-available-text", + description: "The suggestions-available-text attribute", + type: "string", + default: "", + required: false, + }, + { + name: "loading-text", + description: "The loading-text attribute", + type: "string", + default: "", + required: false, + }, + { + name: "label", + description: "The label attribute", + type: "string", + default: "", + required: false, + }, + { + name: "labelledby", + description: "The labelledby attribute", + type: "string", + default: "", + required: false, + }, + { + name: "menu-placement", + description: "The menu-placement attribute", + type: "string", + default: "", + required: false, + }, + { + name: "placeholder", + description: "The placeholder attribute", + type: "string", + default: "", + required: false, + }, + ], + slots: [ + { + name: "list-region", + description: "The list-region slot", + }, + { + name: "menu-region", + description: "The menu-region slot", + }, + { + name: "no-options-region", + description: "The no-options-region slot", + }, + { + name: "loading-region", + description: "The loading-region slot", + }, + ], + }, + ], +}; diff --git a/packages/components/src/picker/scenarios/index.html b/packages/components/src/picker/scenarios/index.html new file mode 100644 index 00000000..f39050b9 --- /dev/null +++ b/packages/components/src/picker/scenarios/index.html @@ -0,0 +1,11 @@ + diff --git a/packages/components/src/progress-ring/index.ts b/packages/components/src/progress-ring/index.ts index a5df470d..5cdded18 100644 --- a/packages/components/src/progress-ring/index.ts +++ b/packages/components/src/progress-ring/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - BaseProgress as ProgressRing, - ProgressRingOptions, - progressRingTemplate as template -} from '@microsoft/fast-foundation'; -import { progressRingStyles as styles } from '@microsoft/fast-components'; + BaseProgress as ProgressRing, + ProgressRingOptions, + progressRingTemplate as template, +} from "@microsoft/fast-foundation"; +import { progressRingStyles as styles } from "./progress-ring.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#BaseProgress} registration for configuring the component with a DesignSystem. @@ -15,13 +12,13 @@ import { progressRingStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpProgressRing = ProgressRing.compose({ - baseName: 'progress-ring', - template, - styles, - indeterminateIndicator: /* html */ ` +export const fastProgressRing = ProgressRing.compose({ + baseName: "progress-ring", + template, + styles, + indeterminateIndicator: /* html */ ` ({ r="7px" > - ` + `, }); /** diff --git a/packages/components/src/progress-ring/progress-ring.stories.ts b/packages/components/src/progress-ring/progress-ring.stories.ts index d8166e29..3ea4ce70 100644 --- a/packages/components/src/progress-ring/progress-ring.stories.ts +++ b/packages/components/src/progress-ring/progress-ring.stories.ts @@ -1,62 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import Examples from "./fixtures/circular.html"; +import "./index.js"; export default { - title: 'Components/Progress Ring', - argTypes: { - min: { control: 'number', min: 0 }, - max: { control: 'number', min: 0 }, - value: { control: 'number', min: 0 }, - paused: { control: 'boolean' }, - height: { control: 'number', min: 4 } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - return ` - `; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - min: null, - max: null, - value: null, - paused: false, - height: null + title: "Progress Ring", }; -export const WithValue: StoryObj = { render: Template.bind({}) }; -WithValue.args = { - ...Default.args, - min: 0, - max: 50, - value: 30 -}; - -export const Paused: StoryObj = { render: Template.bind({}) }; -Paused.args = { - ...WithValue.args, - paused: true -}; +export const ProgressRing = () => Examples; diff --git a/packages/components/src/progress-ring/progress-ring.styles.ts b/packages/components/src/progress-ring/progress-ring.styles.ts new file mode 100644 index 00000000..bf9cae30 --- /dev/null +++ b/packages/components/src/progress-ring/progress-ring.styles.ts @@ -0,0 +1,106 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + display, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + ProgressRingOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + accentForegroundRest, + neutralFillRest, + neutralForegroundHint, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/size.js"; + +/** + * Styles for Progress Ring + * @public + */ +export const progressRingStyles: FoundationElementTemplate< + ElementStyles, + ProgressRingOptions +> = (context, definition) => + css` + ${display("flex")} :host { + align-items: center; + outline: none; + height: calc(${heightNumber} * 1px); + width: calc(${heightNumber} * 1px); + margin: calc(${heightNumber} * 1px) 0; + } + + .progress { + height: 100%; + width: 100%; + } + + .background { + stroke: ${neutralFillRest}; + fill: none; + stroke-width: 2px; + } + + .determinate { + stroke: ${accentForegroundRest}; + fill: none; + stroke-width: 2px; + stroke-linecap: round; + transform-origin: 50% 50%; + transform: rotate(-90deg); + transition: all 0.2s ease-in-out; + } + + .indeterminate-indicator-1 { + stroke: ${accentForegroundRest}; + fill: none; + stroke-width: 2px; + stroke-linecap: round; + transform-origin: 50% 50%; + transform: rotate(-90deg); + transition: all 0.2s ease-in-out; + animation: spin-infinite 2s linear infinite; + } + + :host([paused]) .indeterminate-indicator-1 { + animation-play-state: paused; + stroke: ${neutralFillRest}; + } + + :host([paused]) .determinate { + stroke: ${neutralForegroundHint}; + } + + @keyframes spin-infinite { + 0% { + stroke-dasharray: 0.01px 43.97px; + transform: rotate(0deg); + } + 50% { + stroke-dasharray: 21.99px 21.99px; + transform: rotate(450deg); + } + 100% { + stroke-dasharray: 0.01px 43.97px; + transform: rotate(1080deg); + } + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .indeterminate-indicator-1, + .determinate { + stroke: ${SystemColors.FieldText}; + } + .background { + stroke: ${SystemColors.Field}; + } + :host([paused]) .indeterminate-indicator-1 { + stroke: ${SystemColors.Field}; + } + :host([paused]) .determinate { + stroke: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/progress/index.ts b/packages/components/src/progress/index.ts index e3bbfb98..35d6d017 100644 --- a/packages/components/src/progress/index.ts +++ b/packages/components/src/progress/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - BaseProgress as Progress, - ProgressOptions, - progressTemplate as template -} from '@microsoft/fast-foundation'; -import { progressStyles as styles } from '@microsoft/fast-components'; + BaseProgress as Progress, + ProgressOptions, + progressTemplate as template, +} from "@microsoft/fast-foundation"; +import { progressStyles as styles } from "./progress.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#BaseProgress} registration for configuring the component with a DesignSystem. @@ -15,18 +12,18 @@ import { progressStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpProgress = Progress.compose({ - baseName: 'progress', - template, - styles, - indeterminateIndicator1: /* html */ ` +export const fastProgress = Progress.compose({ + baseName: "progress", + template, + styles, + indeterminateIndicator1: /* html */ ` + + `, + indeterminateIndicator2: /* html */ ` `, - indeterminateIndicator2: /* html */ ` - - ` }); /** diff --git a/packages/components/src/progress/progress.stories.ts b/packages/components/src/progress/progress.stories.ts index 9b68ef84..26c75a7e 100644 --- a/packages/components/src/progress/progress.stories.ts +++ b/packages/components/src/progress/progress.stories.ts @@ -1,67 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import Examples from "./fixtures/linear.html"; +import "./index.js"; export default { - title: 'Components/Progress', - argTypes: { - min: { control: 'number', min: 0 }, - max: { control: 'number', min: 0 }, - value: { control: 'number', min: 0 }, - paused: { control: 'boolean' }, - height: { control: 'number', min: 4 } - }, - parameters: { - actions: { - disabled: true - } - }, - decorators: [ - story => `
- ${story()} -
` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - return ` - `; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - min: null, - max: null, - value: null, - paused: false, - height: null + title: "Progress", }; -export const WithValue: StoryObj = { render: Template.bind({}) }; -WithValue.args = { - ...Default.args, - min: 0, - max: 50, - value: 30 -}; - -export const Paused: StoryObj = { render: Template.bind({}) }; -Paused.args = { - ...WithValue.args, - paused: true -}; +export const Progress = () => Examples; diff --git a/packages/components/src/progress/progress.styles.ts b/packages/components/src/progress/progress.styles.ts new file mode 100644 index 00000000..191f65ce --- /dev/null +++ b/packages/components/src/progress/progress.styles.ts @@ -0,0 +1,147 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + display, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + ProgressOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + accentForegroundRest, + designUnit, + neutralFillRest, + neutralForegroundHint, +} from "../design-tokens.js"; + +/** + * Styles for Progress + * @public + */ +export const progressStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("flex")} :host { + align-items: center; + outline: none; + height: calc(${designUnit} * 1px); + margin: calc(${designUnit} * 1px) 0; + } + + .progress { + background-color: ${neutralFillRest}; + border-radius: calc(${designUnit} * 1px); + width: 100%; + height: 100%; + display: flex; + align-items: center; + position: relative; + } + + .determinate { + background-color: ${accentForegroundRest}; + border-radius: calc(${designUnit} * 1px); + height: 100%; + transition: all 0.2s ease-in-out; + display: flex; + } + + .indeterminate { + height: 100%; + border-radius: calc(${designUnit} * 1px); + display: flex; + width: 100%; + position: relative; + overflow: hidden; + } + + .indeterminate-indicator-1 { + position: absolute; + opacity: 0; + height: 100%; + background-color: ${accentForegroundRest}; + border-radius: calc(${designUnit} * 1px); + animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); + width: 40%; + animation: indeterminate-1 2s infinite; + } + + .indeterminate-indicator-2 { + position: absolute; + opacity: 0; + height: 100%; + background-color: ${accentForegroundRest}; + border-radius: calc(${designUnit} * 1px); + animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); + width: 60%; + animation: indeterminate-2 2s infinite; + } + + :host([paused]) .indeterminate-indicator-1, + :host([paused]) .indeterminate-indicator-2 { + animation-play-state: paused; + background-color: ${neutralFillRest}; + } + + :host([paused]) .determinate { + background-color: ${neutralForegroundHint}; + } + + @keyframes indeterminate-1 { + 0% { + opacity: 1; + transform: translateX(-100%); + } + 70% { + opacity: 1; + transform: translateX(300%); + } + 70.01% { + opacity: 0; + } + 100% { + opacity: 0; + transform: translateX(300%); + } + } + + @keyframes indeterminate-2 { + 0% { + opacity: 0; + transform: translateX(-150%); + } + 29.99% { + opacity: 0; + } + 30% { + opacity: 1; + transform: translateX(-150%); + } + 100% { + transform: translateX(166.66%); + opacity: 1; + } + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .progress { + forced-color-adjust: none; + background-color: ${SystemColors.Field}; + box-shadow: 0 0 0 1px inset ${SystemColors.FieldText}; + } + .determinate, + .indeterminate-indicator-1, + .indeterminate-indicator-2 { + forced-color-adjust: none; + background-color: ${SystemColors.FieldText}; + } + :host([paused]) .determinate, + :host([paused]) .indeterminate-indicator-1, + :host([paused]) .indeterminate-indicator-2 { + background-color: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/radio-group/index.ts b/packages/components/src/radio-group/index.ts index 63dd8360..f77adf67 100644 --- a/packages/components/src/radio-group/index.ts +++ b/packages/components/src/radio-group/index.ts @@ -1,11 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - RadioGroup, - radioGroupTemplate as template -} from '@microsoft/fast-foundation'; -import { radioGroupStyles as styles } from '@microsoft/fast-components'; +import { RadioGroup, radioGroupTemplate as template } from "@microsoft/fast-foundation"; +import { radioGroupStyles as styles } from "./radio-group.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#RadioGroup} registration for configuring the component with a DesignSystem. @@ -14,12 +8,12 @@ import { radioGroupStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpRadioGroup = RadioGroup.compose({ - baseName: 'radio-group', - template, - styles +export const fastRadioGroup = RadioGroup.compose({ + baseName: "radio-group", + template, + styles, }); /** diff --git a/packages/components/src/radio-group/radio-group.stories.ts b/packages/components/src/radio-group/radio-group.stories.ts index 4901138f..de4feeb7 100644 --- a/packages/components/src/radio-group/radio-group.stories.ts +++ b/packages/components/src/radio-group/radio-group.stories.ts @@ -1,83 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import Examples from "./fixtures/base.html"; -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; +import "./index.js"; export default { - title: 'Components/Radio Group', - argTypes: { - isDisabled: { control: 'boolean' }, - isReadOnly: { control: 'boolean' }, - orientation: { control: 'radio', options: ['horizontal', 'vertical'] }, - onChange: { - action: 'changed', - table: { - disable: true - } - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - - Apples - Oranges - Bananas - Kiwi - Grapefruit - Mango - Blueberries - Strawberries - Pineapple - ` - ); - - const radioGroup = container.firstChild as HTMLElement; - - if (args.onChange) { - radioGroup.addEventListener('change', args.onChange); - } - - return radioGroup; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - isDisabled: false, - isReadOnly: false, - orientation: 'horizontal', - onChange: action('radio-onchange') + title: "Radio Group", }; -export const Vertical: StoryObj = { render: Template.bind({}) }; -Vertical.args = { - ...Default.args, - orientation: 'vertical' -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true -}; - -export const WithReadOnly: StoryObj = { render: Template.bind({}) }; -WithReadOnly.args = { - ...Default.args, - isDisabled: true -}; +export const RadioGroup = () => Examples; diff --git a/packages/components/src/radio-group/radio-group.styles.ts b/packages/components/src/radio-group/radio-group.styles.ts new file mode 100644 index 00000000..d0f72a49 --- /dev/null +++ b/packages/components/src/radio-group/radio-group.styles.ts @@ -0,0 +1,28 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { display, FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { designUnit } from "../design-tokens.js"; + +/** + * Styles for Radio Group + * @public + */ +export const radioGroupStyles: FoundationElementTemplate = ( + context, + definition +) => css` + ${display("flex")} :host { + align-items: flex-start; + margin: calc(${designUnit} * 1px) 0; + flex-direction: column; + } + .positioning-region { + display: flex; + flex-wrap: wrap; + } + :host([orientation="vertical"]) .positioning-region { + flex-direction: column; + } + :host([orientation="horizontal"]) .positioning-region { + flex-direction: row; + } +`; diff --git a/packages/components/src/radio/index.ts b/packages/components/src/radio/index.ts index 09885a16..4d88ff72 100644 --- a/packages/components/src/radio/index.ts +++ b/packages/components/src/radio/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - Radio, - RadioOptions, - radioTemplate as template -} from '@microsoft/fast-foundation'; -import { radioStyles as styles } from './radio.styles'; + Radio, + RadioOptions, + radioTemplate as template, +} from "@microsoft/fast-foundation"; +import { radioStyles as styles } from "./radio.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Radio} registration for configuring the component with a DesignSystem. @@ -15,15 +12,15 @@ import { radioStyles as styles } from './radio.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpRadio = Radio.compose({ - baseName: 'radio', - template, - styles, - checkedIndicator: /* html */ ` -
- ` +export const fastRadio = Radio.compose({ + baseName: "radio", + template, + styles, + checkedIndicator: /* html */ ` +
+ `, }); /** diff --git a/packages/components/src/radio/radio.stories.ts b/packages/components/src/radio/radio.stories.ts index d1abb673..d122d0c4 100644 --- a/packages/components/src/radio/radio.stories.ts +++ b/packages/components/src/radio/radio.stories.ts @@ -1,75 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; +import Examples from "./fixtures/base.html"; +import "./index.js"; export default { - title: 'Components/Radio', - argTypes: { - isChecked: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isReadOnly: { control: 'boolean' }, - onChange: { - action: 'changed', - table: { - disable: true - } - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - Radio element - ` - ); - - const radio = container.firstChild as HTMLElement; - - if (args.onChange) { - radio.addEventListener('change', args.onChange); - } - - return radio; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - isChecked: false, - isDisabled: false, - isReadOnly: false, - onChange: action('radio-onchange') -}; - -export const WithChecked: StoryObj = { render: Template.bind({}) }; -WithChecked.args = { - ...Default.args, - isChecked: true + title: "Radio", }; -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true -}; - -export const WithReadOnly: StoryObj = { render: Template.bind({}) }; -WithReadOnly.args = { - ...Default.args, - isDisabled: true -}; +export const Radio = () => Examples; diff --git a/packages/components/src/radio/radio.styles.ts b/packages/components/src/radio/radio.styles.ts index 2a153cb1..cb9cc3f5 100644 --- a/packages/components/src/radio/radio.styles.ts +++ b/packages/components/src/radio/radio.styles.ts @@ -1,230 +1,213 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - RadioOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + RadioOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillActive, - accentFillFocus, - accentFillHover, - accentFillRest, - bodyFont, - designUnit, - disabledOpacity, - focusStrokeWidth, - foregroundOnAccentActive, - foregroundOnAccentHover, - foregroundOnAccentRest, - neutralFillInputActive, - neutralFillInputHover, - neutralFillInputRest, - neutralForegroundRest, - neutralStrokeActive, - neutralStrokeHover, - neutralStrokeRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; - + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + designUnit, + disabledOpacity, + fillColor, + focusStrokeOuter, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillInputActive, + neutralFillInputHover, + neutralFillInputRest, + neutralForegroundRest, + neutralStrokeActive, + neutralStrokeHover, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Radio * @public */ -export const radioStyles: FoundationElementTemplate< - ElementStyles, - RadioOptions -> = (context, definition) => - css` - ${display('inline-flex')} :host { - --input-size: calc((${heightNumber} / 2) + ${designUnit}); - align-items: center; - outline: none; - margin: calc(${designUnit} * 1px) 0; - /* Chromium likes to select label text or the default slot when - the radio is clicked. Maybe there is a better solution here? */ - user-select: none; - position: relative; - flex-direction: row; - transition: all 0.2s ease-in-out; - } - - .control { - position: relative; - width: calc((${heightNumber} / 2 + ${designUnit}) * 1px); - height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); - box-sizing: border-box; - border-radius: 999px; - border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; - background: ${neutralFillInputRest}; - outline: none; - cursor: pointer; - } - - .label { - font-family: ${bodyFont}; - color: ${neutralForegroundRest}; - /* Need to discuss with Brian how HorizontalSpacingNumber can work. - https://github.com/microsoft/fast/issues/2766 */ - padding-inline-start: calc(${designUnit} * 2px + 2px); - margin-inline-end: calc(${designUnit} * 2px + 2px); - cursor: pointer; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - } - - .label__hidden { - display: none; - visibility: hidden; - } - - .control, - .checked-indicator { - flex-shrink: 0; - } - - .checked-indicator { - position: absolute; - top: 5px; - left: 5px; - right: 5px; - bottom: 5px; - border-radius: 999px; - display: inline-block; - background: ${foregroundOnAccentRest}; - fill: ${foregroundOnAccentRest}; - opacity: 0; - pointer-events: none; - } - - :host(:not([disabled])) .control:hover { - background: ${neutralFillInputHover}; - border-color: ${neutralStrokeHover}; - } - - :host(:not([disabled])) .control:active { - background: ${neutralFillInputActive}; - border-color: ${neutralStrokeActive}; - } - - :host(:${focusVisible}) .control { - outline: solid calc(${focusStrokeWidth} * 1px) ${accentFillFocus}; - } - - :host([aria-checked='true']) .control { - background: ${accentFillRest}; - border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; - } - - :host([aria-checked='true']:not([disabled])) .control:hover { - background: ${accentFillHover}; - border: calc(${strokeWidth} * 1px) solid ${accentFillHover}; - } - - :host([aria-checked='true']:not([disabled])) - .control:hover - .checked-indicator { - background: ${foregroundOnAccentHover}; - fill: ${foregroundOnAccentHover}; - } - - :host([aria-checked='true']:not([disabled])) .control:active { - background: ${accentFillActive}; - border: calc(${strokeWidth} * 1px) solid ${accentFillActive}; - } - - :host([aria-checked='true']:not([disabled])) - .control:active - .checked-indicator { - background: ${foregroundOnAccentActive}; - fill: ${foregroundOnAccentActive}; - } - - :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { - outline-offset: 2px; - outline: solid calc(${focusStrokeWidth} * 1px) ${accentFillFocus}; - } - - :host([disabled]) .label, - :host([readonly]) .label, - :host([readonly]) .control, - :host([disabled]) .control { - cursor: ${disabledCursor}; - } - - :host([aria-checked='true']) .checked-indicator { - opacity: 1; - } - - :host([disabled]) { - opacity: ${disabledOpacity}; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - .control, - :host([aria-checked='true']:not([disabled])) .control { - forced-color-adjust: none; - border-color: ${SystemColors.FieldText}; - background: ${SystemColors.Field}; - } - :host(:not([disabled])) .control:hover { - border-color: ${SystemColors.Highlight}; - background: ${SystemColors.Field}; - } - :host([aria-checked='true']:not([disabled])) .control:hover, - :host([aria-checked='true']:not([disabled])) .control:active { - border-color: ${SystemColors.Highlight}; - background: ${SystemColors.Highlight}; - } - :host([aria-checked='true']) .checked-indicator { - background: ${SystemColors.Highlight}; - fill: ${SystemColors.Highlight}; - } - :host([aria-checked='true']:not([disabled])) - .control:hover - .checked-indicator, - :host([aria-checked='true']:not([disabled])) - .control:active +export const radioStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("inline-flex")} :host { + --input-size: calc((${heightNumber} / 2) + ${designUnit}); + align-items: center; + outline: none; + margin: calc(${designUnit} * 1px) 0; + /* Chromium likes to select label text or the default slot when + the radio is clicked. Maybe there is a better solution here? */ + user-select: none; + position: relative; + flex-direction: row; + transition: all 0.2s ease-in-out; + } + + .control { + position: relative; + width: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + box-sizing: border-box; + border-radius: 999px; + border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; + background: ${neutralFillInputRest}; + outline: none; + cursor: pointer; + } + + .label { + font-family: ${bodyFont}; + color: ${neutralForegroundRest}; + padding-inline-start: calc(${designUnit} * 2px + 2px); + margin-inline-end: calc(${designUnit} * 2px + 2px); + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + } + + .label__hidden { + display: none; + visibility: hidden; + } + + .control, .checked-indicator { + flex-shrink: 0; + } + .checked-indicator { - background: ${SystemColors.HighlightText}; - fill: ${SystemColors.HighlightText}; - } - :host(:${focusVisible}) .control { - border-color: ${SystemColors.Highlight}; - outline-offset: 2px; - outline: solid calc(${focusStrokeWidth} * 1px) ${SystemColors.FieldText}; - } - :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { - border-color: ${SystemColors.Highlight}; - outline: solid calc(${focusStrokeWidth} * 1px) ${SystemColors.FieldText}; - } - :host([disabled]) { - forced-color-adjust: none; - opacity: 1; - } - :host([disabled]) .label { - color: ${SystemColors.GrayText}; - } - :host([disabled]) .control, - :host([aria-checked='true'][disabled]) .control:hover, - .control:active { - background: ${SystemColors.Field}; - border-color: ${SystemColors.GrayText}; - } - :host([disabled]) .checked-indicator, - :host([aria-checked='true'][disabled]) .control:hover .checked-indicator { - fill: ${SystemColors.GrayText}; - background: ${SystemColors.GrayText}; - } - `) - ); + position: absolute; + top: 5px; + left: 5px; + right: 5px; + bottom: 5px; + border-radius: 999px; + display: inline-block; + background: ${foregroundOnAccentRest}; + fill: ${foregroundOnAccentRest}; + opacity: 0; + pointer-events: none; + } + + :host(:not([disabled])) .control:hover{ + background: ${neutralFillInputHover}; + border-color: ${neutralStrokeHover}; + } + + :host(:not([disabled])) .control:active { + background: ${neutralFillInputActive}; + border-color: ${neutralStrokeActive}; + } + + :host(:${focusVisible}) .control { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } + + :host([aria-checked="true"]) .control { + background: ${accentFillRest}; + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + } + + :host([aria-checked="true"]:not([disabled])) .control:hover { + background: ${accentFillHover}; + border: calc(${strokeWidth} * 1px) solid ${accentFillHover}; + } + + :host([aria-checked="true"]:not([disabled])) .control:hover .checked-indicator { + background: ${foregroundOnAccentHover}; + fill: ${foregroundOnAccentHover}; + } + + :host([aria-checked="true"]:not([disabled])) .control:active { + background: ${accentFillActive}; + border: calc(${strokeWidth} * 1px) solid ${accentFillActive}; + } + + :host([aria-checked="true"]:not([disabled])) .control:active .checked-indicator { + background: ${foregroundOnAccentActive}; + fill: ${foregroundOnAccentActive}; + } + + :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } + + :host([disabled]) .label, + :host([readonly]) .label, + :host([readonly]) .control, + :host([disabled]) .control { + cursor: ${disabledCursor}; + } + + :host([aria-checked="true"]) .checked-indicator { + opacity: 1; + } + + :host([disabled]) { + opacity: ${disabledOpacity}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .control, + :host([aria-checked="true"]:not([disabled])) .control { + forced-color-adjust: none; + border-color: ${SystemColors.FieldText}; + background: ${SystemColors.Field}; + } + :host(:not([disabled])) .control:hover { + border-color: ${SystemColors.Highlight}; + background: ${SystemColors.Field}; + } + :host([aria-checked="true"]:not([disabled])) .control:hover, + :host([aria-checked="true"]:not([disabled])) .control:active { + border-color: ${SystemColors.Highlight}; + background: ${SystemColors.Highlight}; + } + :host([aria-checked="true"]) .checked-indicator { + background: ${SystemColors.Highlight}; + fill: ${SystemColors.Highlight}; + } + :host([aria-checked="true"]:not([disabled])) .control:hover .checked-indicator, + :host([aria-checked="true"]:not([disabled])) .control:active .checked-indicator { + background: ${SystemColors.HighlightText}; + fill: ${SystemColors.HighlightText}; + } + :host(:${focusVisible}) .control { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + :host([aria-checked="true"]:${focusVisible}:not([disabled])) .control { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + :host([disabled]) { + forced-color-adjust: none; + opacity: 1; + } + :host([disabled]) .label { + color: ${SystemColors.GrayText}; + } + :host([disabled]) .control, + :host([aria-checked="true"][disabled]) .control:hover, .control:active { + background: ${SystemColors.Field}; + border-color: ${SystemColors.GrayText}; + } + :host([disabled]) .checked-indicator, + :host([aria-checked="true"][disabled]) .control:hover .checked-indicator { + fill: ${SystemColors.GrayText}; + background: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/search/index.ts b/packages/components/src/search/index.ts index 40b0f97e..200d49de 100644 --- a/packages/components/src/search/index.ts +++ b/packages/components/src/search/index.ts @@ -1,15 +1,30 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - +import { attr } from "@microsoft/fast-element"; import { - Search as FoundationSearch, - searchTemplate as template -} from '@microsoft/fast-foundation'; -import { Search } from '@microsoft/fast-components'; -import { searchStyles as styles } from './search.styles'; + Search as FoundationSearch, + searchTemplate as template, +} from "@microsoft/fast-foundation"; +import { searchStyles as styles } from "./search.styles.js"; + +/** + * Search appearances + * @public + */ +export type SearchAppearance = "filled" | "outline"; -// TODO -// we need to add error/invalid +/** + * @internal + */ +export class Search extends FoundationSearch { + /** + * The appearance of the element. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: SearchAppearance = "outline"; +} /** * A function that returns a {@link @microsoft/fast-foundation#Search} registration for configuring the component with a DesignSystem. @@ -18,24 +33,22 @@ import { searchStyles as styles } from './search.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: \ * * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus} */ -export const jpSearch = Search.compose({ - baseName: 'search', - baseClass: FoundationSearch, - template, - styles, - shadowOptions: { - delegatesFocus: true - } +export const fastSearch = Search.compose({ + baseName: "search", + baseClass: FoundationSearch, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, }); -export { Search, SearchAppearance } from '@microsoft/fast-components'; - /** * Styles for Search * @public */ -export { styles as searchStyles }; +export const searchStyles = styles; diff --git a/packages/components/src/search/search.stories.ts b/packages/components/src/search/search.stories.ts index f7c63cd7..efef6d1f 100644 --- a/packages/components/src/search/search.stories.ts +++ b/packages/components/src/search/search.stories.ts @@ -1,124 +1,23 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { getFaIcon, setTheme } from '../utilities/storybook'; -import { Search } from './index'; - -export default { - title: 'Components/Search', - argTypes: { - label: { control: 'text' }, - placeholder: { control: 'text' }, - value: { control: 'text' }, - maxLength: { control: 'number' }, - size: { control: 'number' }, - isReadOnly: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isAutoFocused: { control: 'boolean' }, - searchIcon: { control: 'boolean' }, - appearance: { control: 'radio', options: ['outline', 'filled'] }, - onChange: { - action: 'changed', - table: { - disable: true - } +import { STORY_RENDERED } from "@storybook/core-events"; +import addons from "@storybook/addons"; +import SearchTemplate from "./fixtures/search.html"; +import "./index.js"; + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("search")) { + document.querySelectorAll(".form").forEach((el: HTMLFormElement) => { + el.onsubmit = event => { + console.log(event, "event"); + event.preventDefault(); + const form: HTMLFormElement = event.target as HTMLFormElement; + console.log(form.elements["fname"].value, "value of input"); + }; + }); } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.label} - ${args.searchIcon ? getFaIcon('search', 'end') : ''} - ` - ); - - const search = container.firstChild as Search; - - if (args.value) { - search.value = args.value; - } +}); - if (args.onChange) { - search.addEventListener('change', args.onChange); - } - - return search; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Search Label', - placeholder: '', - value: '', - maxLength: '', - size: '', - isReadOnly: false, - isDisabled: false, - isAutoFocused: false, - appearance: 'outline', - searchIcon: false, - onChange: action('search-onchange') -}; - -export const WithPlaceholder: StoryObj = { render: Template.bind({}) }; -WithPlaceholder.args = { - ...Default.args, - placeholder: 'Placeholder Text' -}; - -export const WithAutofocus: StoryObj = { render: Template.bind({}) }; -WithAutofocus.args = { - ...Default.args, - autofocus: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - disabled: true -}; - -export const WithSize: StoryObj = { render: Template.bind({}) }; -WithSize.args = { - ...Default.args, - placeholder: 'This search is 50 characters in width', - size: 50 -}; - -export const WithMaxLength: StoryObj = { render: Template.bind({}) }; -WithMaxLength.args = { - ...Default.args, - placeholder: 'This search field can only contain a maximum of 10 characters', - maxLength: 10 -}; - -export const WithReadonly: StoryObj = { render: Template.bind({}) }; -WithReadonly.args = { - ...Default.args, - readonly: true +export default { + title: "Search", }; -export const WithSearchIcon: StoryObj = { render: Template.bind({}) }; -WithSearchIcon.args = { - ...Default.args, - searchIcon: true -}; +export const Search = () => SearchTemplate; diff --git a/packages/components/src/search/search.styles.ts b/packages/components/src/search/search.styles.ts index 4328188c..8b60178c 100644 --- a/packages/components/src/search/search.styles.ts +++ b/packages/components/src/search/search.styles.ts @@ -1,147 +1,281 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; +import { DesignToken } from "@microsoft/fast-foundation"; import { - DesignToken, - focusVisible, - FoundationElementTemplate, - TextFieldOptions -} from '@microsoft/fast-foundation'; -import { Swatch } from '../color'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + TextFieldOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { Swatch } from "../color/swatch.js"; import { - bodyFont, - controlCornerRadius, - density, - neutralFillRecipe, - neutralFillStealthActive, - neutralFillStealthHover, - neutralFillStealthRecipe, - neutralForegroundRest, - typeRampBaseFontSize, - typeRampBaseLineHeight, - designUnit -} from '../design-tokens'; -import { BaseFieldStyles, heightNumber } from '../styles/index'; - -const clearButtonHover = DesignToken.create( - 'clear-button-hover' -).withDefault((target: HTMLElement) => { - const buttonRecipe = neutralFillStealthRecipe.getValueFor(target); - const inputRecipe = neutralFillRecipe.getValueFor(target); - return buttonRecipe.evaluate(target, inputRecipe.evaluate(target).hover) - .hover; -}); - -const clearButtonActive = DesignToken.create( - 'clear-button-active' -).withDefault((target: HTMLElement) => { - const buttonRecipe = neutralFillStealthRecipe.getValueFor(target); - const inputRecipe = neutralFillRecipe.getValueFor(target); - return buttonRecipe.evaluate(target, inputRecipe.evaluate(target).hover) - .active; -}); - -export const searchStyles: FoundationElementTemplate< - ElementStyles, - TextFieldOptions -> = (context, definition) => css` - ${BaseFieldStyles} - - .control { - padding: 0; - padding-inline-start: calc(${designUnit} * 2px + 1px); - padding-inline-end: calc( - (${designUnit} * 2px) + (${heightNumber} * 1px) + 1px - ); - } + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + density, + designUnit, + disabledOpacity, + fillColor, + focusStrokeOuter, + neutralFillHover, + neutralFillInputHover, + neutralFillInputRest, + neutralFillRecipe, + neutralFillStealthActive, + neutralFillStealthHover, + neutralFillStealthRecipe, + neutralForegroundRest, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +const clearButtonHover = DesignToken.create("clear-button-hover").withDefault( + (target: HTMLElement) => { + const buttonRecipe = neutralFillStealthRecipe.getValueFor(target); + const inputRecipe = neutralFillRecipe.getValueFor(target); + return buttonRecipe.evaluate(target, inputRecipe.evaluate(target).hover).hover; + } +); + +const clearButtonActive = DesignToken.create("clear-button-active").withDefault( + (target: HTMLElement) => { + const buttonRecipe = neutralFillStealthRecipe.getValueFor(target); + const inputRecipe = neutralFillRecipe.getValueFor(target); + return buttonRecipe.evaluate(target, inputRecipe.evaluate(target).hover).active; + } +); - .control::-webkit-search-cancel-button { - -webkit-appearance: none; - } +export const searchStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("inline-block")} :host { + font-family: ${bodyFont}; + outline: none; + user-select: none; + } - .control:hover, + .root { + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: row; + color: ${neutralForegroundRest}; + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + height: calc(${heightNumber} * 1px); + align-items: baseline; + } + + .control { + -webkit-appearance: none; + font: inherit; + background: transparent; + border: 0; + color: inherit; + height: calc(100% - 4px); + width: 100%; + margin-top: auto; + margin-bottom: auto; + border: none; + padding: 0 calc(${designUnit} * 2px + 1px); + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + } + + .control::-webkit-search-cancel-button { + -webkit-appearance: none; + } + + .control:hover, .control:${focusVisible}, .control:disabled, .control:active { - outline: none; - } - - .clear-button { - height: calc(100% - 2px); - opacity: 0; - margin: 1px; - background: transparent; - color: ${neutralForegroundRest}; - fill: currentcolor; - border: none; - border-radius: calc(${controlCornerRadius} * 1px); - min-width: calc(${heightNumber} * 1px); - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - outline: none; - font-family: ${bodyFont}; - padding: 0 calc((10 + (${designUnit} * 2 * ${density})) * 1px); - } - - .clear-button:hover { - background: ${neutralFillStealthHover}; - } - - .clear-button:active { - background: ${neutralFillStealthActive}; - } - - :host([appearance='filled']) .clear-button:hover { - background: ${clearButtonHover}; - } - - :host([appearance='filled']) .clear-button:active { - background: ${clearButtonActive}; - } - - .input-wrapper { - display: flex; - position: relative; - width: 100%; - } - - .start, - .end { - display: flex; - margin: 1px; - } - - ::slotted([slot='end']) { - height: 100%; - } - - .end { - margin-inline-end: 1px; - } - - ::slotted(svg) { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - margin-inline-end: 11px; - margin-inline-start: 11px; - margin-top: auto; - margin-bottom: auto; - } - - .clear-button__hidden { - opacity: 0; - } - - :host(:hover:not([disabled], [readOnly])) .clear-button, - :host(:active:not([disabled], [readOnly])) .clear-button, - :host(:focus-within:not([disabled], [readOnly])) .clear-button { - opacity: 1; - } - - :host(:hover:not([disabled], [readOnly])) .clear-button__hidden, - :host(:active:not([disabled], [readOnly])) .clear-button__hidden, - :host(:focus-within:not([disabled], [readOnly])) .clear-button__hidden { - opacity: 0; - } -`; + outline: none; + } + + .clear-button { + height: calc(100% - 2px); + opacity: 0; + margin: 1px; + background: transparent; + color: ${neutralForegroundRest}; + fill: currentcolor; + border: none; + border-radius: calc(${controlCornerRadius} * 1px); + min-width: calc(${heightNumber} * 1px); + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + outline: none; + font-family: ${bodyFont}; + padding: 0 calc((10 + (${designUnit} * 2 * ${density})) * 1px); + } + + .clear-button:hover { + background: ${neutralFillStealthHover}; + } + + .clear-button:active { + background: ${neutralFillStealthActive}; + } + + :host([appearance="filled"]) .clear-button:hover { + background: ${clearButtonHover}; + } + + :host([appearance="filled"]) .clear-button:active { + background: ${clearButtonActive}; + } + + .input-wrapper { + display: flex; + position: relative; + width: 100%; + height: 100%; + } + + .label { + display: block; + color: ${neutralForegroundRest}; + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + margin-bottom: 4px; + } + + .label__hidden { + display: none; + visibility: hidden; + } + + .input-wrapper, + .start, + .end { + align-self: center; + } + + .start, + .end { + display: flex; + margin: 1px; + fill: currentcolor; + } + + ::slotted([slot="end"]) { + height: 100% + } + + .end { + margin-inline-end: 1px; + height: calc(100% - 2px); + } + + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + margin-inline-end: 11px; + margin-inline-start: 11px; + margin-top: auto; + margin-bottom: auto; + } + + :host(:hover:not([disabled])) .root { + background: ${neutralFillInputHover}; + border-color: ${accentFillHover}; + } + + :host(:active:not([disabled])) .root { + background: ${neutralFillInputHover}; + border-color: ${accentFillActive}; + } + + :host(:focus-within:not([disabled])) .root { + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 1px ${focusStrokeOuter} inset; + } + + .clear-button__hidden { + opacity: 0; + } + + :host(:hover:not([disabled], [readOnly])) .clear-button, + :host(:active:not([disabled], [readOnly])) .clear-button, + :host(:focus-within:not([disabled], [readOnly])) .clear-button { + opacity: 1; + } + + :host(:hover:not([disabled], [readOnly])) .clear-button__hidden, + :host(:active:not([disabled], [readOnly])) .clear-button__hidden, + :host(:focus-within:not([disabled], [readOnly])) .clear-button__hidden { + opacity: 0; + } + + :host([appearance="filled"]) .root { + background: ${fillColor}; + } + + :host([appearance="filled"]:hover:not([disabled])) .root { + background: ${neutralFillHover}; + } + + :host([disabled]) .label, + :host([readonly]) .label, + :host([readonly]) .control, + :host([disabled]) .control { + cursor: ${disabledCursor}; + } + + :host([disabled]) { + opacity: ${disabledOpacity}; + } + + :host([disabled]) .control { + border-color: ${neutralStrokeRest}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .root, + :host([appearance="filled"]) .root { + forced-color-adjust: none; + background: ${SystemColors.Field}; + border-color: ${SystemColors.FieldText}; + } + :host(:hover:not([disabled])) .root, + :host([appearance="filled"]:hover:not([disabled])) .root, + :host([appearance="filled"]:hover) .root { + background: ${SystemColors.Field}; + border-color: ${SystemColors.Highlight}; + } + .start, + .end { + fill: currentcolor; + } + :host([disabled]) { + opacity: 1; + } + :host([disabled]) .root, + :host([appearance="filled"]:hover[disabled]) .root { + border-color: ${SystemColors.GrayText}; + background: ${SystemColors.Field}; + } + :host(:focus-within:enabled) .root { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 1px ${SystemColors.Highlight} inset; + } + input::placeholder { + color: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/select/index.ts b/packages/components/src/select/index.ts index 5f90000c..d80ecbdc 100644 --- a/packages/components/src/select/index.ts +++ b/packages/components/src/select/index.ts @@ -1,117 +1,188 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { attr } from '@microsoft/fast-element'; +import { css, ElementStyles, observable } from "@microsoft/fast-element"; +import { + Select as FoundationSelect, + SelectOptions, + selectTemplate as template, +} from "@microsoft/fast-foundation"; import { - Select as FoundationSelect, - SelectOptions, - selectTemplate as template -} from '@microsoft/fast-foundation'; -import { selectStyles as styles } from './select.styles'; + fillColor, + heightNumberAsToken, + neutralLayerFloating, +} from "../design-tokens.js"; +import { selectStyles as styles } from "./select.styles.js"; /** - * Base class for Select + * Base class for Select. * @public */ export class Select extends FoundationSelect { - /** - * Whether the select has a compact layout or not. - * - * @public - * @remarks - * HTML Attribute: minimal - */ - @attr({ attribute: 'autowidth', mode: 'boolean' }) - public autoWidth: boolean; - - /** - * Whether the select has a compact layout or not. - * - * @public - * @remarks - * HTML Attribute: minimal - */ - @attr({ attribute: 'minimal', mode: 'boolean' }) - public minimal: boolean; - - /** - * The connected callback for this FASTElement. - * - * @override - * - * @internal - */ - connectedCallback(): void { - super.connectedCallback(); - this.setAutoWidth(); - } - - /** - * Synchronize the form-associated proxy and updates the value property of the element. - * - * @param prev - the previous collection of slotted option elements - * @param next - the next collection of slotted option elements - * - * @internal - */ - slottedOptionsChanged(prev: Element[] | undefined, next: Element[]): void { - super.slottedOptionsChanged(prev, next); - this.setAutoWidth(); - } - - /** - * (Un-)set the width when the autoWidth property changes. - * - * @param prev - the previous autoWidth value - * @param next - the current autoWidth value - */ - protected autoWidthChanged(prev: boolean | undefined, next: boolean): void { - if (next) { - this.setAutoWidth(); - } else { - this.style.removeProperty('width'); + /** + * An internal stylesheet to hold calculated CSS custom properties. + * + * @internal + */ + private computedStylesheet?: ElementStyles; + + /** + * @internal + */ + public connectedCallback(): void { + super.connectedCallback(); + + if (this.listbox) { + fillColor.setValueFor(this.listbox, neutralLayerFloating); + } + } + + /** + * Returns the calculated max height for the listbox. + * + * @internal + * @remarks + * Used to generate the `--listbox-max-height` CSS custom property. + * + */ + private get listboxMaxHeight(): string { + return Math.floor( + this.maxHeight / heightNumberAsToken.getValueFor(this) + ).toString(); } - } - - /** - * Compute the listbox width to set the one of the input. - */ - protected setAutoWidth(): void { - if (!this.autoWidth || !this.isConnected) { - return; + + /** + * The cached scroll width of the listbox when visible. + * + * @internal + */ + @observable + private listboxScrollWidth: string = ""; + + /** + * @internal + */ + protected listboxScrollWidthChanged(): void { + this.updateComputedStylesheet(); + } + + /** + * Returns the size value, if any. Otherwise, returns 4 if in + * multi-selection mode, or 0 if in single-selection mode. + * + * @internal + * @remarks + * Used to generate the `--size` CSS custom property. + * + */ + private get selectSize(): string { + return `${this.size ?? (this.multiple ? 4 : 0)}`; + } + + /** + * Updates the computed stylesheet when the multiple property changes. + * + * @param prev - the previous multiple value + * @param next - the current multiple value + * + * @override + * @internal + */ + public multipleChanged(prev: boolean | undefined, next: boolean): void { + super.multipleChanged(prev, next); + this.updateComputedStylesheet(); } - let listWidth = this.listbox.getBoundingClientRect().width; - // If the list has not been displayed yet trick to get its size - if (listWidth === 0 && this.listbox.hidden) { - Object.assign(this.listbox.style, { visibility: 'hidden' }); - this.listbox.removeAttribute('hidden'); - listWidth = this.listbox.getBoundingClientRect().width; - this.listbox.setAttribute('hidden', ''); - this.listbox.style.removeProperty('visibility'); + /** + * Sets the selectMaxSize design token when the maxHeight property changes. + * + * @param prev - the previous maxHeight value + * @param next - the current maxHeight value + * + * @internal + */ + protected maxHeightChanged(prev: number | undefined, next: number): void { + if (this.collapsible) { + this.updateComputedStylesheet(); + } } - if (listWidth > 0) { - Object.assign(this.style, { width: `${listWidth}px` }); + public setPositioning(): void { + super.setPositioning(); + this.updateComputedStylesheet(); + } + + /** + * Updates the component dimensions when the size property is changed. + * + * @param prev - the previous size value + * @param next - the current size value + * + * @override + * @internal + */ + protected sizeChanged(prev: number | undefined, next: number): void { + super.sizeChanged(prev, next); + this.updateComputedStylesheet(); + + if (this.collapsible) { + requestAnimationFrame(() => { + this.listbox.style.setProperty("display", "flex"); + this.listbox.style.setProperty("overflow", "visible"); + this.listbox.style.setProperty("visibility", "hidden"); + this.listbox.style.setProperty("width", "auto"); + this.listbox.hidden = false; + + this.listboxScrollWidth = `${this.listbox.scrollWidth}`; + + this.listbox.hidden = true; + this.listbox.style.removeProperty("display"); + this.listbox.style.removeProperty("overflow"); + this.listbox.style.removeProperty("visibility"); + this.listbox.style.removeProperty("width"); + }); + + return; + } + + this.listboxScrollWidth = ""; + } + + /** + * Updates an internal stylesheet with calculated CSS custom properties. + * + * @internal + */ + protected updateComputedStylesheet(): void { + if (this.computedStylesheet) { + this.$fastController.removeStyles(this.computedStylesheet); + } + + this.computedStylesheet = css` + :host { + --listbox-max-height: ${this.listboxMaxHeight}; + --listbox-scroll-width: ${this.listboxScrollWidth}; + --size: ${this.selectSize}; + } + `; + + this.$fastController.addStyles(this.computedStylesheet); } - } } /** - * A function that returns a Select registration for configuring the component with a DesignSystem. + * A function that returns a {@link @microsoft/fast-foundation#Select} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#selectTemplate} * * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * */ -export const jpSelect = Select.compose({ - baseName: 'select', - baseClass: FoundationSelect, - template, - styles, - indicator: /* html */ ` +export const fastSelect = Select.compose({ + baseName: "select", + baseClass: FoundationSelect, + template, + styles, + indicator: /* html */ ` ({ d="M11.85.65c.2.2.2.5 0 .7L6.4 6.84a.55.55 0 01-.78 0L.14 1.35a.5.5 0 11.71-.7L6 5.8 11.15.65c.2-.2.5-.2.7 0z" /> - ` + `, }); export { styles as selectStyles }; diff --git a/packages/components/src/select/select.pw.spec.ts b/packages/components/src/select/select.pw.spec.ts new file mode 100644 index 00000000..73475352 --- /dev/null +++ b/packages/components/src/select/select.pw.spec.ts @@ -0,0 +1,322 @@ +import type { + ListboxOption as FASTOption, + Select as FASTSelectType +} from "@microsoft/fast-foundation"; +import { ArrowKeys } from "@microsoft/fast-web-utilities"; +import chai from "chai"; +import type { ElementHandle } from "playwright"; + +const { expect } = chai; + +type FASTSelect = HTMLElement & FASTSelectType; + +describe("FASTSelect", function () { + beforeEach(async function () { + if (!this.page && !this.browser) { + this.skip(); + } + + this.documentHandle = await this.page.evaluateHandle(() => document); + + this.setupHandle = await this.page.evaluateHandle( + (document) => { + const element = document.createElement("fast-select") as FASTSelect; + + for (let i = 1; i <= 3; i++) { + const option = document.createElement("fast-option") as FASTOption; + option.value = `${i}`; + option.textContent = `option ${i}`; + element.appendChild(option); + } + + document.body.appendChild(element) + }, + this.documentHandle + ); + }); + + afterEach(async function () { + if (this.setupHandle) { + await this.setupHandle.dispose(); + } + }); + + // FASTSelect should render on the page + it("should render on the page", async function () { + const element = await this.page.waitForSelector("fast-select"); + + expect(element).to.exist; + }); + + // FASTSelect should have a value of 'one' + it("should have a value of 'one'", async function () { + const element = await this.page.waitForSelector("fast-select"); + expect(await element?.evaluate(node => (node as FASTSelect).value)).to.equal("1"); + }); + + // FASTSelect should have a text content of 'option 1' + it("should have a text content of 'option 1'", async function () { + const element = await this.page.waitForSelector("fast-select .selected-value"); + expect(await element?.evaluate((node: HTMLElement) => node.innerText)).to.equal( + "option 1" + ); + }); + + // FASTSelect should open when focused and receives keyboard interaction + describe("should open when focused and receives keyboard interaction", function () { + // FASTSelect should open when focused and receives keyboard interaction via space key + it("via Space key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + expect(await element.evaluate(node => node.open)).to.be.false; + + await element.focus(); + + await this.page.keyboard.press(" "); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await this.page.keyboard.press(" "); + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + + // FASTSelect should open when focused and receives keyboard interaction via enter key + it("via Enter key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + expect(await element.evaluate(node => node.open)).to.be.false; + + await element.focus(); + + await element.press("Enter"); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await element.press("Enter"); + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + }); + + // FASTSelect should close + describe("should close", function () { + // FASTSelect should close when focused and keyboard interaction is received + describe("when focused and keyboard interaction is received", function () { + // FASTSelect should close when focused and keyboard interaction is received via space key + it("via Space key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.press(" "); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await element.press(" "); + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + + // FASTSelect should close when focused and keyboard interaction is received via enter key + it("via Enter key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.press("Enter"); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await element.press("Enter"); + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + + // FASTSelect should close when focused and keyboard interaction is received via escape key + it("via Escape key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.click(); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await this.page.keyboard.press("Escape"); + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + + // FASTSelect should close when focused and keyboard interaction is received via tab key + it("via Tab key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.click(); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await element.press("Tab"); + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + }); + + describe("when focus is lost", function () { + it("via click", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.click(); + + expect(await element.evaluate(node => node.open)).to.be.true; + + await this.page.click("body"); + + expect( + await this.page.evaluate( + element => element.isSameNode(document.activeElement), + element + ) + ).to.be.false; + + expect(await element.evaluate(node => node.open)).to.be.false; + }); + }); + }); + + describe("should emit an event when focused and receives keyboard interaction", function () { + describe("while closed", function () { + for (const direction of Object.values(ArrowKeys)) { + describe(`via ${direction} key`, function () { + for (const eventName of ["change", "input"]) { + it(`of type '${eventName}'`, async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await this.page.exposeFunction("sendEvent", type => + expect(type).to.equal(eventName) + ); + + await element.evaluate((node, eventName) => { + node.addEventListener( + eventName, + async (e: CustomEvent) => + await window["sendEvent"](e.type) + ); + }, eventName); + + await element.press(direction); + }); + } + }); + } + }); + }); + + describe("should change the value when focused and receives keyboard interaction", function () { + it("via arrow down key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + expect(await element.evaluate(node => node.value)).to.equal("1"); + + await element.press("ArrowDown"); + + expect(await element.evaluate(node => node.value)).to.equal("2"); + }); + + it("via arrow up key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.evaluate(node => (node.value = "2")); + + expect(await element.evaluate(node => node.value)).to.equal("2"); + + await element.press("ArrowUp"); + + expect(await element.evaluate(node => node.value)).to.equal("1"); + }); + + it("via home key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.evaluate(node => (node.value = "3")); + + expect(await element.evaluate(node => node.value)).to.equal("3"); + + await element.press("Home"); + + expect(await element.evaluate(node => node.value)).to.equal("1"); + }); + + it("via end key", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + expect(await element.evaluate(node => node.value)).to.equal("1"); + + await element.press("End"); + + expect(await element.evaluate(node => node.value)).to.equal("3"); + }); + }); + + describe("when opened", function () { + it("should scroll the selected option into view", async function () { + const element = (await this.page.waitForSelector( + "fast-select" + )) as ElementHandle; + + await element.evaluate(element => { + element.innerHTML = ""; + for (let i = 0; i < 50; i++) { + const option = document.createElement("fast-option") as FASTOption; + option.value = `${i}`; + option.textContent = `option ${i}`; + element.appendChild(option); + } + }); + + const selectedOption = (await element.$(".listbox")) as ElementHandle< + FASTOption + >; + + await element.evaluate(node => (node.selectedIndex = 35)); + + expect( + await element.evaluate(node => node.firstSelectedOption.value) + ).to.equal("35"); + + await element.click(); + + await selectedOption.waitForElementState("visible"); + + expect( + await selectedOption.evaluate(node => node.scrollTop) + ).to.be.closeTo(451, 16); + + await element.evaluate(node => (node.selectedIndex = 0)); + + await element.waitForElementState("stable"); + + expect( + await selectedOption.evaluate(node => node.scrollTop) + ).to.be.closeTo(6, 16); + }); + }); +}); diff --git a/packages/components/src/select/select.stories.ts b/packages/components/src/select/select.stories.ts index 5a6f5d67..67a28027 100644 --- a/packages/components/src/select/select.stories.ts +++ b/packages/components/src/select/select.stories.ts @@ -1,102 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import Examples from "./fixtures/base.html"; +import Multiselect from "./fixtures/multiselect.html"; export default { - title: 'Components/Select', - argTypes: { - isOpen: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - customIndicator: { control: 'boolean' }, - numberOfChildren: { control: 'number' }, - isMinimal: { control: 'boolean' }, - hasAutoWidth: { control: 'boolean' }, - onChange: { - action: 'changed', - table: { - disable: true - } - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - - const index = args.numberOfChildren ?? 3; - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.customIndicator ? getFaIcon('sliders-h', 'indicator') : ''} - ${new Array(args.numberOfChildren ?? 3) - .fill(0) - .map( - (_, index) => - `Option ${index + 1}` - ) - .join('\n')} - This is a very long option ${ - index + 1 - } - ` - ); - - const select = container.firstChild as HTMLElement; - - if (args.isOpen) { - select.setAttribute('open', ''); - } - - if (args.onChange) { - select.addEventListener('change', args.onChange); - } - - return select; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - isOpen: false, - isDisabled: false, - customIndicator: false, - numberOfChildren: 3, - isMinimal: false, - hasAutoWidth: false, - onChange: action('select-onchange') -}; - -export const WithOpen: StoryObj = { render: Template.bind({}) }; -WithOpen.args = { - ...Default.args, - isOpen: true -}; - -export const WithAutoWidth: StoryObj = { render: Template.bind({}) }; -WithAutoWidth.args = { - ...Default.args, - hasAutoWidth: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true + title: "Select", }; -export const WithCustomIndicator: StoryObj = { render: Template.bind({}) }; -WithCustomIndicator.args = { - ...Default.args, - customIndicator: true -}; +export const Select = () => Examples; +export const SelectMultiple = () => Multiselect; diff --git a/packages/components/src/select/select.styles.ts b/packages/components/src/select/select.styles.ts index b58678cd..9e29263e 100644 --- a/packages/components/src/select/select.styles.ts +++ b/packages/components/src/select/select.styles.ts @@ -1,276 +1,298 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { - accentFillActive, - accentFillFocus, - bodyFont, - controlCornerRadius, - designUnit, - disabledOpacity, - focusStrokeWidth, - neutralFillInputActive, - neutralFillInputHover, - neutralFillInputRest, - neutralFillStealthRest, - neutralFillStrongHover, - neutralFillStrongRest, - neutralForegroundRest, - neutralLayerFloating, - neutralStrokeRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '@microsoft/fast-components'; -import type { ElementStyles } from '@microsoft/fast-element'; -import { css } from '@microsoft/fast-element'; +import type { ElementStyles } from "@microsoft/fast-element"; +import { css } from "@microsoft/fast-element"; import type { - FoundationElementTemplate, - SelectOptions -} from '@microsoft/fast-foundation'; + FoundationElementTemplate, + SelectOptions, +} from "@microsoft/fast-foundation"; +import { + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + ListboxOption, + Select, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; -import { elevation, heightNumber } from '../styles'; + accentFillActive, + accentFillFocus, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeInner, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentFocus, + neutralFillInputActive, + neutralFillInputHover, + neutralFillInputRest, + neutralFillStealthRest, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { listboxStyles } from "../listbox/listbox.styles.js"; +import { elevation } from "../styles/elevation.js"; +import { heightNumber } from "../styles/size.js"; /** - * Styles for Select + * Styles for Select. + * * @public */ -export const selectStyles: FoundationElementTemplate< - ElementStyles, - SelectOptions -> = (context, definition) => - css` - ${display('inline-flex')} :host { - --elevation: 14; - background: ${neutralFillInputRest}; - border-radius: calc(${controlCornerRadius} * 1px); - border: calc(${strokeWidth} * 1px) solid ${neutralFillStrongRest}; - box-sizing: border-box; - color: ${neutralForegroundRest}; - font-family: ${bodyFont}; - height: calc(${heightNumber} * 1px); - position: relative; - user-select: none; - outline: none; - vertical-align: top; - } - - :host(:not([autowidth])) { - min-width: 250px; - } - - .listbox { - ${elevation} - background: ${neutralLayerFloating}; - border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; - border-radius: calc(${controlCornerRadius} * 1px); - box-sizing: border-box; - display: inline-flex; - flex-direction: column; - left: 0; - max-height: calc(var(--max-height) - (${heightNumber} * 1px)); - padding: calc(${designUnit} * 1px) 0; - overflow-y: auto; - position: absolute; - z-index: 1; - } - - :host(:not([autowidth])) .listbox { - width: 100%; - } - - :host([autowidth]) ::slotted([role='option']), - :host([autowidth]) ::slotted(option) { - padding: 0 calc(1em + ${designUnit} * 1.25px + 1px); - } - - .listbox[hidden] { - display: none; - } - - .control { - align-items: center; - box-sizing: border-box; - cursor: pointer; - display: flex; - font-size: ${typeRampBaseFontSize}; - font-family: inherit; - line-height: ${typeRampBaseLineHeight}; - min-height: 100%; - padding: 0 calc(${designUnit} * 2.25px); - width: 100%; - } - - :host([minimal]) { - --density: -4; - } - - :host(:not([disabled]):hover) { - background: ${neutralFillInputHover}; - border-color: ${neutralFillStrongHover}; - } - - :host(:${focusVisible}) { - border-color: ${accentFillFocus}; - box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) - ${accentFillFocus}; - } - - :host([disabled]) { - cursor: ${disabledCursor}; - opacity: ${disabledOpacity}; - } - - :host([disabled]) .control { - cursor: ${disabledCursor}; - user-select: none; - } - - :host([disabled]:hover) { - background: ${neutralFillStealthRest}; - color: ${neutralForegroundRest}; - fill: currentcolor; - } - - :host(:not([disabled])) .control:active { - background: ${neutralFillInputActive}; - border-color: ${accentFillActive}; - border-radius: calc(${controlCornerRadius} * 1px); - } - - :host([open][position='above']) .listbox { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - - :host([open][position='below']) .listbox { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - - :host([open][position='above']) .listbox { - border-bottom: 0; - bottom: calc(${heightNumber} * 1px); - } - - :host([open][position='below']) .listbox { - border-top: 0; - top: calc(${heightNumber} * 1px); - } - - .selected-value { - flex: 1 1 auto; - font-family: inherit; - text-align: start; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - - .indicator { - flex: 0 0 auto; - margin-inline-start: 1em; - } - - slot[name='listbox'] { - display: none; - width: 100%; - } - - :host([open]) slot[name='listbox'] { - display: flex; - position: absolute; - ${elevation} - } - - .end { - margin-inline-start: auto; - } - - .start, - .end, - .indicator, - .select-indicator, - ::slotted(svg) { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - fill: currentcolor; - height: 1em; - min-height: calc(${designUnit} * 4px); - min-width: calc(${designUnit} * 4px); - width: 1em; - } - - ::slotted([role='option']), - ::slotted(option) { - flex: 0 0 auto; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host(:not([disabled]):hover), - :host(:not([disabled]):active) { - border-color: ${SystemColors.Highlight}; - } - - :host(:not([disabled]):${focusVisible}) { - background-color: ${SystemColors.ButtonFace}; - box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) - ${SystemColors.Highlight}; - color: ${SystemColors.ButtonText}; - fill: currentcolor; - forced-color-adjust: none; - } - - :host(:not([disabled]):${focusVisible}) .listbox { - background: ${SystemColors.ButtonFace}; - } - - :host([disabled]) { - border-color: ${SystemColors.GrayText}; - background-color: ${SystemColors.ButtonFace}; - color: ${SystemColors.GrayText}; - fill: currentcolor; - opacity: 1; - forced-color-adjust: none; - } - - :host([disabled]:hover) { - background: ${SystemColors.ButtonFace}; - } - - :host([disabled]) .control { - color: ${SystemColors.GrayText}; - border-color: ${SystemColors.GrayText}; - } - - :host([disabled]) .control .select-indicator { - fill: ${SystemColors.GrayText}; - } - - :host(:${focusVisible}) ::slotted([aria-selected="true"][role="option"]), - :host(:${focusVisible}) ::slotted(option[aria-selected="true"]), - :host(:${focusVisible}) ::slotted([aria-selected="true"][role="option"]:not([disabled])) { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.ButtonText}; - box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) - ${SystemColors.HighlightText}; - color: ${SystemColors.HighlightText}; - fill: currentcolor; - } - - .start, - .end, - .indicator, - .select-indicator, - ::slotted(svg) { - color: ${SystemColors.ButtonText}; - fill: currentcolor; - } - `) - ); +export const selectStyles: FoundationElementTemplate = ( + context, + definition +) => { + const selectContext = context.name === context.tagFor(Select); + + // The expression interpolations present in this block cause Prettier to generate + // various formatting bugs. + // prettier-ignore + return css` + ${display("inline-flex")} + + :host { + --elevation: 14; + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + box-sizing: border-box; + color: ${neutralForegroundRest}; + font-family: ${bodyFont}; + height: calc(${heightNumber} * 1px); + position: relative; + user-select: none; + min-width: 250px; + outline: none; + vertical-align: top; + } + + ${selectContext ? css` + :host(:not([aria-haspopup])) { + --elevation: 0; + border: 0; + height: auto; + min-width: 0; + } + ` : ""} + + ${listboxStyles(context, definition)} + + :host .listbox { + ${elevation} + border: none; + display: flex; + left: 0; + position: absolute; + width: 100%; + z-index: 1; + } + + .control + .listbox { + --stroke-size: calc(${designUnit} * ${strokeWidth} * 2); + max-height: calc( + (var(--listbox-max-height) * ${heightNumber} + var(--stroke-size)) * 1px + ); + } + + ${selectContext ? css` + :host(:not([aria-haspopup])) .listbox { + left: auto; + position: static; + z-index: auto; + } + ` : ""} + + .listbox[hidden] { + display: none; + } + + .control { + align-items: center; + box-sizing: border-box; + cursor: pointer; + display: flex; + font-size: ${typeRampBaseFontSize}; + font-family: inherit; + line-height: ${typeRampBaseLineHeight}; + min-height: 100%; + padding: 0 calc(${designUnit} * 2.25px); + width: 100%; + } + + :host(:not([disabled]):hover) { + background: ${neutralFillInputHover}; + border-color: ${accentFillHover}; + } + + :host(:${focusVisible}) { + border-color: ${focusStrokeOuter}; + } + + :host(:not([size]):not([multiple]):not([open]):${focusVisible}), + :host([multiple]:${focusVisible}), + :host([size]:${focusVisible}) { + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${focusStrokeOuter}; + } + + :host(:not([multiple]):not([size]):${focusVisible}) ::slotted(${context.tagFor( + ListboxOption + )}[aria-selected="true"]:not([disabled])) { + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) inset ${focusStrokeInner}; + border-color: ${focusStrokeOuter}; + background: ${accentFillFocus}; + color: ${foregroundOnAccentFocus}; + } + + :host([disabled]) { + cursor: ${disabledCursor}; + opacity: ${disabledOpacity}; + } + + :host([disabled]) .control { + cursor: ${disabledCursor}; + user-select: none; + } + + :host([disabled]:hover) { + background: ${neutralFillStealthRest}; + color: ${neutralForegroundRest}; + fill: currentcolor; + } + + :host(:not([disabled])) .control:active { + background: ${neutralFillInputActive}; + border-color: ${accentFillActive}; + border-radius: calc(${controlCornerRadius} * 1px); + } + + :host([open][position="above"]) .listbox { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: 0; + bottom: calc(${heightNumber} * 1px); + } + + :host([open][position="below"]) .listbox { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: 0; + top: calc(${heightNumber} * 1px); + } + + .selected-value { + flex: 1 1 auto; + font-family: inherit; + min-width: calc(var(--listbox-scroll-width, 0) - (${designUnit} * 4) * 1px); + overflow: hidden; + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; + } + + .indicator { + flex: 0 0 auto; + margin-inline-start: 1em; + } + + slot[name="listbox"] { + display: none; + width: 100%; + } + + :host([open]) slot[name="listbox"] { + display: flex; + position: absolute; + ${elevation} + } + + .end { + margin-inline-start: auto; + } + + .start, + .end, + .indicator, + .select-indicator, + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + fill: currentcolor; + height: 1em; + min-height: calc(${designUnit} * 4px); + min-width: calc(${designUnit} * 4px); + width: 1em; + } + + ::slotted([role="option"]), + ::slotted(option) { + flex: 0 0 auto; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host(:not([disabled]):hover), + :host(:not([disabled]):active) { + border-color: ${SystemColors.Highlight}; + } + + :host(:not([disabled]):${focusVisible}) { + background-color: ${SystemColors.ButtonFace}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${SystemColors.Highlight}; + color: ${SystemColors.ButtonText}; + fill: currentcolor; + forced-color-adjust: none; + } + + :host(:not([disabled]):${focusVisible}) .listbox { + background: ${SystemColors.ButtonFace}; + } + + :host([disabled]) { + border-color: ${SystemColors.GrayText}; + background-color: ${SystemColors.ButtonFace}; + color: ${SystemColors.GrayText}; + fill: currentcolor; + opacity: 1; + forced-color-adjust: none; + } + + :host([disabled]:hover) { + background: ${SystemColors.ButtonFace}; + } + + :host([disabled]) .control { + color: ${SystemColors.GrayText}; + border-color: ${SystemColors.GrayText}; + } + + :host([disabled]) .control .select-indicator { + fill: ${SystemColors.GrayText}; + } + + :host(:${focusVisible}) ::slotted([aria-selected="true"][role="option"]), + :host(:${focusVisible}) ::slotted(option[aria-selected="true"]), + :host(:${focusVisible}) ::slotted([aria-selected="true"][role="option"]:not([disabled])) { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.ButtonText}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) inset ${SystemColors.HighlightText}; + color: ${SystemColors.HighlightText}; + fill: currentcolor; + } + + .start, + .end, + .indicator, + .select-indicator, + ::slotted(svg) { + color: ${SystemColors.ButtonText}; + fill: currentcolor; + } + ` + ) + ); +}; diff --git a/packages/components/src/skeleton/fixtures/base.html b/packages/components/src/skeleton/fixtures/base.html new file mode 100644 index 00000000..a72fc2e1 --- /dev/null +++ b/packages/components/src/skeleton/fixtures/base.html @@ -0,0 +1,120 @@ + + +

Skeleton

+ +

Used as element blocks

+
+ + + + + +
+ +

Used as element blocks with shimmer effect element

+
+ + + + + +
+ +

Using SVG via Pattern attribute

+ + +

Using inline SVG

+ + + + + + + + + + + + + + diff --git a/packages/components/src/skeleton/index.ts b/packages/components/src/skeleton/index.ts new file mode 100644 index 00000000..477b1890 --- /dev/null +++ b/packages/components/src/skeleton/index.ts @@ -0,0 +1,25 @@ +import { Skeleton, skeletonTemplate as template } from "@microsoft/fast-foundation"; +import { skeletonStyles as styles } from "./skeleton.styles.js"; + +/** + * A function that returns a {@link @microsoft/fast-foundation#Skeleton} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#skeletonTemplate} + * + * + * @public + * @remarks + * Generates HTML Element: `` + */ +export const fastSkeleton = Skeleton.compose({ + baseName: "skeleton", + template, + styles, +}); + +/** + * Base class for Skeleton + * @public + */ +export { Skeleton }; + +export { styles as skeletonStyles }; diff --git a/packages/components/src/skeleton/scenarios/index.html b/packages/components/src/skeleton/scenarios/index.html new file mode 100644 index 00000000..3cc56ee5 --- /dev/null +++ b/packages/components/src/skeleton/scenarios/index.html @@ -0,0 +1,53 @@ + diff --git a/packages/components/src/skeleton/skeleton.stories.ts b/packages/components/src/skeleton/skeleton.stories.ts new file mode 100644 index 00000000..f5d9a79d --- /dev/null +++ b/packages/components/src/skeleton/skeleton.stories.ts @@ -0,0 +1,8 @@ +import SkeletonTemplate from "./fixtures/base.html"; +import "./index.js"; + +export default { + title: "Skeleton", +}; + +export const Skeleton = () => SkeletonTemplate; diff --git a/packages/components/src/skeleton/skeleton.styles.ts b/packages/components/src/skeleton/skeleton.styles.ts new file mode 100644 index 00000000..5902f17d --- /dev/null +++ b/packages/components/src/skeleton/skeleton.styles.ts @@ -0,0 +1,106 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { display } from "@microsoft/fast-foundation"; +import { controlCornerRadius, neutralFillRest } from "../design-tokens.js"; + +/** + * Styles for Skeleton + * @public + */ +export const skeletonStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("block")} :host { + --skeleton-fill-default: #e1dfdd; + overflow: hidden; + width: 100%; + position: relative; + background-color: var(--skeleton-fill, var(--skeleton-fill-default)); + --skeleton-animation-gradient-default: linear-gradient( + 270deg, + var(--skeleton-fill, var(--skeleton-fill-default)) 0%, + #f3f2f1 51.13%, + var(--skeleton-fill, var(--skeleton-fill-default)) 100% + ); + --skeleton-animation-timing-default: ease-in-out; + } + + :host([shape="rect"]) { + border-radius: calc(${controlCornerRadius} * 1px); + } + + :host([shape="circle"]) { + border-radius: 100%; + overflow: hidden; + } + + object { + position: absolute; + width: 100%; + height: auto; + z-index: 2; + } + + object img { + width: 100%; + height: auto; + } + + ${display("block")} span.shimmer { + position: absolute; + width: 100%; + height: 100%; + background-image: var( + --skeleton-animation-gradient, + var(--skeleton-animation-gradient-default) + ); + background-size: 0px 0px / 90% 100%; + background-repeat: no-repeat; + background-color: var(--skeleton-animation-fill, ${neutralFillRest}); + animation: shimmer 2s infinite; + animation-timing-function: var( + --skeleton-animation-timing, + var(--skeleton-timing-default) + ); + animation-direction: normal; + z-index: 1; + } + + ::slotted(svg) { + z-index: 2; + } + + ::slotted(.pattern) { + width: 100%; + height: 100%; + } + + @keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + forced-color-adjust: none; + background-color: ${SystemColors.ButtonFace}; + box-shadow: 0 0 0 1px ${SystemColors.ButtonText}; + } + + ${display("block")} span.shimmer { + display: none; + } + ` + ) + ); diff --git a/packages/components/src/slider-label/index.ts b/packages/components/src/slider-label/index.ts index 424650a0..43b402cd 100644 --- a/packages/components/src/slider-label/index.ts +++ b/packages/components/src/slider-label/index.ts @@ -1,16 +1,28 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - horizontalSliderLabelStyles, - SliderLabel, - sliderLabelStyles as styles, - verticalSliderLabelStyles -} from '@microsoft/fast-components'; + SliderLabel as FoundationSliderLabel, + sliderLabelTemplate as template, +} from "@microsoft/fast-foundation"; +import { Orientation } from "@microsoft/fast-web-utilities"; import { - SliderLabel as FoundationSliderLabel, - sliderLabelTemplate as template -} from '@microsoft/fast-foundation'; + horizontalSliderLabelStyles, + sliderLabelStyles as styles, + verticalSliderLabelStyles, +} from "./slider-label.styles.js"; + +/** + * @internal + */ +export class SliderLabel extends FoundationSliderLabel { + protected sliderOrientationChanged(): void { + if (this.sliderOrientation === Orientation.horizontal) { + this.$fastController.addStyles(horizontalSliderLabelStyles); + this.$fastController.removeStyles(verticalSliderLabelStyles); + } else { + this.$fastController.addStyles(verticalSliderLabelStyles); + this.$fastController.removeStyles(horizontalSliderLabelStyles); + } + } +} /** * A function that returns a {@link @microsoft/fast-foundation#SliderLabel} registration for configuring the component with a DesignSystem. @@ -19,18 +31,18 @@ import { * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpSliderLabel = SliderLabel.compose({ - baseName: 'slider-label', - baseClass: FoundationSliderLabel, - template, - styles + +export const fastSliderLabel = SliderLabel.compose({ + baseName: "slider-label", + baseClass: FoundationSliderLabel, + template, + styles, }); export { - SliderLabel, - horizontalSliderLabelStyles, - styles as sliderLabelStyles, - verticalSliderLabelStyles + horizontalSliderLabelStyles, + styles as sliderLabelStyles, + verticalSliderLabelStyles, }; diff --git a/packages/components/src/slider-label/slider-label.stories.ts b/packages/components/src/slider-label/slider-label.stories.ts index a57974c8..46406153 100644 --- a/packages/components/src/slider-label/slider-label.stories.ts +++ b/packages/components/src/slider-label/slider-label.stories.ts @@ -1,52 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import Examples from "./fixtures/base.html"; +import "./index.js"; export default { - title: 'Components/Slider Label', - argTypes: { - hideMark: { control: 'boolean' }, - disabled: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - Label - `; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - hideMark: false, - position: null, - disabled: false -}; - -export const WithHideMark: StoryObj = { render: Template.bind({}) }; -WithHideMark.args = { - ...Default.args, - hideMark: true + title: "Slider Label", }; -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - disabled: true -}; +export const SliderLabel = () => Examples; diff --git a/packages/components/src/slider-label/slider-label.styles.ts b/packages/components/src/slider-label/slider-label.styles.ts new file mode 100644 index 00000000..86bec4e5 --- /dev/null +++ b/packages/components/src/slider-label/slider-label.styles.ts @@ -0,0 +1,122 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + display, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { + bodyFont, + designUnit, + disabledOpacity, + neutralForegroundRest, + neutralStrokeRest, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; + +/** + * Styles for Horizontal Slider label + * @public + */ +export const horizontalSliderLabelStyles = css` + :host { + align-self: start; + grid-row: 2; + margin-top: -2px; + height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + width: auto; + } + .container { + grid-template-rows: auto auto; + grid-template-columns: 0; + } + .label { + margin: 2px 0; + } +`; + +/** + * Styles for Vertical slider label + * @public + */ +export const verticalSliderLabelStyles = css` + :host { + justify-self: start; + grid-column: 2; + margin-left: 2px; + height: auto; + width: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + } + .container { + grid-template-columns: auto auto; + grid-template-rows: 0; + min-width: calc(var(--thumb-size) * 1px); + height: calc(var(--thumb-size) * 1px); + } + .mark { + transform: rotate(90deg); + align-self: center; + } + .label { + margin-left: calc((${designUnit} / 2) * 3px); + align-self: center; + } +`; + +/** + * Styles for Slider Label + * @public + */ +export const sliderLabelStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("block")} :host { + font-family: ${bodyFont}; + color: ${neutralForegroundRest}; + fill: currentcolor; + } + .root { + position: absolute; + display: grid; + } + .container { + display: grid; + justify-self: center; + } + .label { + justify-self: center; + align-self: center; + white-space: nowrap; + max-width: 30px; + } + .mark { + width: calc((${designUnit} / 4) * 1px); + height: calc(${heightNumber} * 0.25 * 1px); + background: ${neutralStrokeRest}; + justify-self: center; + } + :host(.disabled) { + opacity: ${disabledOpacity}; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .mark { + forced-color-adjust: none; + background: ${SystemColors.FieldText}; + } + :host(.disabled) { + forced-color-adjust: none; + opacity: 1; + } + :host(.disabled) .label { + color: ${SystemColors.GrayText}; + } + :host(.disabled) .mark { + background: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/slider/images/slider-rtl.png b/packages/components/src/slider/images/slider-rtl.png new file mode 100644 index 0000000000000000000000000000000000000000..2c63be7f6585e6365927d5d096334d4a800bdf0b GIT binary patch literal 5274 zcmb_gXIK;4)<%yg3J3}UN;@b;5DiF^5~9I|AW=Z65)qKzl}_*|Mfw3mijaIr35XbJ zQ6vyZ4g>@V5_$_Aq!=KSkdSaE-g}%3a=`V*%#xbT6Cu#USAEdbyU~ojpknzyEF(Q=ys#QCRv%A_V=^HJg zWTpvIj{EjTCr$<&_E(f0aqX1e6E|zDmks^Z@I8*9=qyeP@X*pV`lZjpEf&kes)Fj!h!Ne;&?I zyzPNBhv=wm=sYhU8+aSTsrt $@aCU!=VNAS%-9;AbmeJ;n4@HHxY{$t>rBY3UkT zfRg)7V8^B*>?!?~@YeE>NNdtb5uf@M?4>Q#_DQ54XLpCeX-O0(V(^?(GU3aWA@Iup zaD;Oh-0>7yhz#`*@f0$Ov%`>yLFM%4iUvs3?=F^OT4yKblETuY7Nu-{n;IoV25;Ms z(s1DyJPxzHrBOA>Lr#A&CBmZ^tCN?6BtEQvzaiTa^ibb@N!g;rK1lVb)Ft88WXXX5 zRYq3Obr-+e-g^MlF=WV|0akG2rrmv{te432sLwpV{T)YMcTOc?}Yvip-Sb(KzrkV12= z|IS>lCg;vG;q1}go6npYdHKcuTI+goP|%~!T{A|0D<<(J5ZyRINo{*EBp)&Qzbx;6 zupc_!d*xXetzp}CE0o$8>5nSS2$FlK6n$cp4|7X!?@X6w!N4KXScYNqylW zE@8k_Y4olViwTrbD^%Folri2A?!*q2t1`DE#d$AN@lg^6A%3HZi!<=ynb|D3&*7W{ zUReLWclZ2d2n z{8Xp5N@u;1R)pVpm8!RKL*K`vT}x;bLJwL0{>6^&eOe>SC$CG-f)xPcpAWOu4)>AI zq*P<<^h-iX<^5Y^&-!I;XR^s~EPDoip`TL6-%pB>Uz!Qdr%o^PGHV|L>q}V|-?5?b zwq6bEAtS`k(K?e0`#-Cvb(@9oYznV09R6?>rqY~wnMgidklC~~7EING)k$rRN6^2i z|EX|5GHkDQGNAGX3l2L&JGk@>E7_{m(Du3Yv2iAZp4lG*g zu!1KEF^oWh+Q*tP++M$(Zwn9H`#~lvC#C(6Y=mFCZY%H5$|tG}sH7>B5Fx4KlY|i= zsL%6l-p}a2!UtP=Xs4Sxh+jtA=4EY<1;Yi1r9nQHX5=>@TK0W@c5O|R!-&g3JK_#( zYn#{p)#hSf`E0hO9d+~3-{7Qf9flq_^!&gTaLkkE(mvys@L2mEP2#wr;MJ=Y8RgL5?lIuez2rf;}_MM;1^}EVA zbN2BpuePwTjW~EXD9h-5F5@Qg?*iJmTwiU56|6bry|G4;CGktf64W{sy>%LB_HEzt^>w;X=m1Dr z>edwH)%z#!2SNR;tr1@m|F^Gb$znEaBpW@f*SNdh(UIb$=V~6Q`X=~qROX~Hwj&VC zO4ZT~&bduQJodd(=Cwqz7OW}#-oNA0jdBjRIV@omD-iFP;b6c2EYW>ILBZhJomR%f z*vg`{LH*#LNwi~{d{RR&kt&70G$oLxT>V%X2iM&Zpo;2m#zmWBA&Eain^W}YizSV? z6{xLZUkfgjWo>g~ys?K9OpOd49T0--svT{%#Umo6;_|Wa+NA<%nYQ|drP$;lo=#r5 zhP91%LXz65oo{qNN3Gs&8+ZqMl1`V6l0Cdp6^WrXioTK*j)kvQUw_)bocn{a#(peF zAMD$RW71Qs&ab3?WvUDqONZi}UVRNrPdwX|DB1k4)yh1i>-3*Re_j@XKtxVuXllN_ zSSoxP>pdby!id~I*^?ey=qKl2ybcxNhS#?O4FSiOUX)h|rTiE|$3gVvasGiX<_AM*c6=hylY7@(^`=RO-ls*6uJ3yuHp=a<57UG z*t%ioP1+%Rti-m?Tj?ud#JN=A>GhYT0_>5GNll6&A|;CR6lZxLRJ!UPYQIoc}oS2)@~a=kk{9r%~@vi_w5&>RIyqO?;Wix z%{ydo@ZAmtoz9Hr$cINXg zfDgUXfF97vDM4?mJ&ALec;oX$xf2Q2kChC2krSsrYcLjld9B9;)N2m7(9?)G6d=NK8&ev-wLDsidIb?C`#WP^3Pq4Zg><5_S+od5EV zM`u9U1p#_6JvjsQgLzMj(veM2W0Vo z|2g@2y`2yC=dGn#k!)-jkh|{VYwwEL-|vt37qrBv%!B74n|?_<2Of6i>H5o9{#)?d zQdq^LSFUv;K0Ow;e4GupK#C$=c$e?N(8Nc8WnAjY+osSLLw|-rr2#J$jry-lzWw82 z#{Sz#+?|6Zu0{}vLbC-W`D$q|p({$8p8=fY^z)Gq2=*QsUUJ#RE@fFv3ZmMfA0>S* z^`8ix`KaENyH!}RZE z^MeVpQM>-e`F|(R`4zayh`(dAZiWwI4*s*b4KaVEHgG*molB;*o=HmY#}sSsmaNB{ zUNfKB!o+atblE;pttab;k2g>wE*ddFD!;h(hx*geX0dCl8#1v?MU{)wt;vb0!ne0k zqM5%K_`ri>P$j3yliP?9?@zU(Z<@fThgh(LufJG&S@-{m$^Wk2JPk6e#0-zOCgbBMl^m!t{vIs-fu1>mvbEYQ7ln~U8z+SpMP-kU4Dnw411)2b` z86qx}PKZ7%LRk1-IE}|W)}u*s6pZ76&p%~OCL`xeCaDu3 z;t9d06_iVeV(6X06A$x3m6ro0KXm1n{!2f?M7vUOVlG?kX6s zq;l_x!loSEy81R&tzP#|!yvusD~F*RuzA&YkZyiabhBE7~Ff zkPDvOT-Q)V!|{kZInFKq%{!+JUITccVJENy8vwGVZj$Z6 zzj+t5DakdMZ56xtv_XP9EN;G~y|KMAqN9U@dlwiQaee4TYep;thol0-(a%r`6eJ3f zt>J1Wq4w~yPY8Y(8rf^YjzuK4JgTcKo{ZammOQig7vQ<-z$d>KD*(MGGB=lquaqqU zh^#)b#X^V0?duNQ+5W}IKV-Gy5XL>p%^Dk&X}wS0Bd;aL0RZ4(UTE@SR?!xb3uUYt zmjU{%&$JI}Y;i~(1W;wQfIpnCZ;ln6rbpaA zb0*0(oRtbteL$B5_K}xebzh^AULbq{Jh=@|R>`=ohr*oQK$8Uf!V8P{Kx9`fz5St! zD-a;dE=4f>1{=bICDb2X_4)YEY(s`E&sr7IY1Lng(z;xY@WTp##g|?Y7-she1wH*y z%7;))LVSOFtpJ8HtdoPB7a-+Th@shhnlu<@QCttXA<)draTAue+&slv)qJ*M$fKn0IRXOr6 z)2-_cwI)ciO1kLo$y{QVNAW65$gml)GF+P_T7v#a-c@y-u)53GwsGmXIUo290NxUE zU<S9wVHzwgVHsFPCRsE*COqha@Vp>sE1eelE9@8m!4+hys5ZhM&9tQz zSfI@c0O>rrgu$oil$cz@2F`b_P9=i+!TJND%;1&b6W5a@9M#OrR=tbC;af@gm&g$n zmBu_zpfRGpfU)~@!C<}Z%$hIO@@mBTSMkp8_2ke_-j#Rv%5$li2{AO&lYo5V79HUB z4Rt6)Qa6IJ$`EiX6IjsAH3$vJR(HA{vQj%Bz*gbya_Vx0#NV??k^$=>C^f(nU5`j7 z9TJieh-7XkUta*$rO1WRxg>c_X~qGj0*(Eh`}HIR!`+ywoB0=0 zA)F9vZZpKPEQH$~h&?KO>MKtGYk_?p?(5739t{{dh@ By)^&; literal 0 HcmV?d00001 diff --git a/packages/components/src/slider/index.ts b/packages/components/src/slider/index.ts index e970d89c..9df99de8 100644 --- a/packages/components/src/slider/index.ts +++ b/packages/components/src/slider/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - Slider, - SliderOptions, - sliderTemplate as template -} from '@microsoft/fast-foundation'; -import { sliderStyles as styles } from './slider.styles'; + Slider, + SliderOptions, + sliderTemplate as template, +} from "@microsoft/fast-foundation"; +import { sliderStyles as styles } from "./slider.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Slider} registration for configuring the component with a DesignSystem. @@ -15,15 +12,15 @@ import { sliderStyles as styles } from './slider.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpSlider = Slider.compose({ - baseName: 'slider', - template, - styles, - thumb: /* html */ ` +export const fastSlider = Slider.compose({ + baseName: "slider", + template, + styles, + thumb: /* html */ `
- ` + `, }); /** diff --git a/packages/components/src/slider/slider.stories.ts b/packages/components/src/slider/slider.stories.ts index a836cda4..1309a658 100644 --- a/packages/components/src/slider/slider.stories.ts +++ b/packages/components/src/slider/slider.stories.ts @@ -1,75 +1,24 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; - -export default { - title: 'Components/Slider', - argTypes: { - value: { control: 'range', min: 0, max: 10, step: 5 }, - orientation: { control: 'radio', options: ['horizontal', 'vertical'] }, - disabled: { control: 'boolean' }, - readonly: { control: 'boolean' }, - onChange: { - action: 'changed', - table: { - disable: true - } +import addons from "@storybook/addons"; +import { STORY_RENDERED } from "@storybook/core-events"; +import type { Slider as FoundationSlider } from "@microsoft/fast-foundation"; +import Examples from "./fixtures/base.html"; +import "./index.js"; + +function valueTextFormatter(value: string): string { + return `${value} degrees celsius`; +} + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().endsWith("slider")) { + ["switcher", "switcher2", "slider1"].forEach(x => { + const slider = document.getElementById(x) as FoundationSlider; + slider.valueTextFormatter = valueTextFormatter; + }); } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - 0% - 10% - 90% - 100% - ` - ); - - const slider = container.firstChild as HTMLElement; - - if (args.onChange) { - slider.addEventListener('change', args.onChange); - } +}); - return slider; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - orientation: 'horizontal', - disabled: false, - readonly: false, - value: 70, - onChange: action('slider-onchange') -}; - -export const Vertical: StoryObj = { render: Template.bind({}) }; -Vertical.args = { - ...Default.args, - orientation: 'vertical' +export default { + title: "Slider", }; -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - disabled: true -}; +export const Slider = () => Examples; diff --git a/packages/components/src/slider/slider.styles.ts b/packages/components/src/slider/slider.styles.ts index 4f62bfd6..7c6d8520 100644 --- a/packages/components/src/slider/slider.styles.ts +++ b/packages/components/src/slider/slider.styles.ts @@ -1,175 +1,189 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - SliderOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + SliderOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillFocus, - controlCornerRadius, - designUnit, - disabledOpacity, - fillColor, - focusStrokeWidth, - neutralForegroundRest, - neutralStrokeHover, - neutralStrokeRest -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentForegroundRest, + controlCornerRadius, + designUnit, + disabledOpacity, + fillColor, + focusStrokeOuter, + neutralForegroundRest, + neutralStrokeHover, + neutralStrokeRest, +} from "../design-tokens.js"; +import { DirectionalStyleSheetBehavior, heightNumber } from "../styles/index.js"; + +const ltr = css` + .track-start { + left: 0; + } +`; + +const rtl = css` + .track-start { + right: 0; + } +`; /** * Styles for Slider * @public */ -export const sliderStyles: FoundationElementTemplate< - ElementStyles, - SliderOptions -> = (context, definition) => - css` - :host([hidden]) { - display: none; - } +export const sliderStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + :host([hidden]) { + display: none; + } - ${display('inline-grid')} :host { - --thumb-size: calc(${heightNumber} * 0.5 - ${designUnit}); - --thumb-translate: calc( - var(--thumb-size) * -0.5 + var(--track-width) / 2 - ); - --track-overhang: calc((${designUnit} / 2) * -1); - --track-width: ${designUnit}; - --jp-slider-height: calc(var(--thumb-size) * 10); - align-items: center; - width: 100%; - margin: calc(${designUnit} * 1px) 0; - user-select: none; - box-sizing: border-box; - border-radius: calc(${controlCornerRadius} * 1px); - outline: none; - cursor: pointer; - } - :host([orientation='horizontal']) .positioning-region { - position: relative; - margin: 0 8px; - display: grid; - grid-template-rows: calc(var(--thumb-size) * 1px) 1fr; - } - :host([orientation='vertical']) .positioning-region { - position: relative; - margin: 0 8px; - display: grid; - height: 100%; - grid-template-columns: calc(var(--thumb-size) * 1px) 1fr; - } + ${display("inline-grid")} :host { + --thumb-size: calc(${heightNumber} * 0.5 - ${designUnit}); + --thumb-translate: calc(var(--thumb-size) * -0.5 + var(--track-width) / 2); + --track-overhang: calc((${designUnit} / 2) * -1); + --track-width: ${designUnit}; + --fast-slider-height: calc(var(--thumb-size) * 10); + align-items: center; + width: 100%; + margin: calc(${designUnit} * 1px) 0; + user-select: none; + box-sizing: border-box; + border-radius: calc(${controlCornerRadius} * 1px); + outline: none; + cursor: pointer; + } + :host([orientation="horizontal"]) .positioning-region { + position: relative; + margin: 0 8px; + display: grid; + grid-template-rows: calc(var(--thumb-size) * 1px) 1fr; + } + :host([orientation="vertical"]) .positioning-region { + position: relative; + margin: 0 8px; + display: grid; + height: 100%; + grid-template-columns: calc(var(--thumb-size) * 1px) 1fr; + } - :host(:${focusVisible}) .thumb-cursor { - box-shadow: - 0 0 0 2px ${fillColor}, - 0 0 0 calc((2 + ${focusStrokeWidth}) * 1px) ${accentFillFocus}; - } + :host(:${focusVisible}) .thumb-cursor { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } - .thumb-container { - position: absolute; - height: calc(var(--thumb-size) * 1px); - width: calc(var(--thumb-size) * 1px); - transition: all 0.2s ease; - color: ${neutralForegroundRest}; - fill: currentcolor; - } - .thumb-cursor { - border: none; - width: calc(var(--thumb-size) * 1px); - height: calc(var(--thumb-size) * 1px); - background: ${neutralForegroundRest}; - border-radius: calc(${controlCornerRadius} * 1px); - } - .thumb-cursor:hover { - background: ${neutralForegroundRest}; - border-color: ${neutralStrokeHover}; - } - .thumb-cursor:active { - background: ${neutralForegroundRest}; - } - :host([orientation='horizontal']) .thumb-container { - transform: translateX(calc(var(--thumb-size) * 0.5px)) - translateY(calc(var(--thumb-translate) * 1px)); - } - :host([orientation='vertical']) .thumb-container { - transform: translateX(calc(var(--thumb-translate) * 1px)) - translateY(calc(var(--thumb-size) * 0.5px)); - } - :host([orientation='horizontal']) { - min-width: calc(var(--thumb-size) * 1px); - } - :host([orientation='horizontal']) .track { - right: calc(var(--track-overhang) * 1px); - left: calc(var(--track-overhang) * 1px); - align-self: start; - height: calc(var(--track-width) * 1px); - } - :host([orientation='vertical']) .track { - top: calc(var(--track-overhang) * 1px); - bottom: calc(var(--track-overhang) * 1px); - width: calc(var(--track-width) * 1px); - height: 100%; - } - .track { - background: ${neutralStrokeRest}; - position: absolute; - border-radius: calc(${controlCornerRadius} * 1px); - } - :host([orientation='vertical']) { - height: calc(var(--jp-slider-height) * 1px); - min-height: calc(var(--thumb-size) * 1px); - min-width: calc(${designUnit} * 20px); - } - :host([disabled]), - :host([readonly]) { - cursor: ${disabledCursor}; - } - :host([disabled]) { - opacity: ${disabledOpacity}; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - .thumb-cursor { - forced-color-adjust: none; - border-color: ${SystemColors.FieldText}; - background: ${SystemColors.FieldText}; - } - .thumb-cursor:hover, - .thumb-cursor:active { - background: ${SystemColors.Highlight}; - } - .track { - forced-color-adjust: none; - background: ${SystemColors.FieldText}; - } - :host(:${focusVisible}) .thumb-cursor { - border-color: ${SystemColors.Highlight}; - } - :host([disabled]) { - opacity: 1; - } - :host([disabled]) .track, - :host([disabled]) .thumb-cursor { - forced-color-adjust: none; - background: ${SystemColors.GrayText}; - } + .thumb-container { + position: absolute; + height: calc(var(--thumb-size) * 1px); + width: calc(var(--thumb-size) * 1px); + transition: all 0.2s ease; + color: ${neutralForegroundRest}; + fill: currentcolor; + } + .thumb-cursor { + border: none; + width: calc(var(--thumb-size) * 1px); + height: calc(var(--thumb-size) * 1px); + background: ${neutralForegroundRest}; + border-radius: calc(${controlCornerRadius} * 1px); + } + .thumb-cursor:hover { + background: ${neutralForegroundRest}; + border-color: ${neutralStrokeHover}; + } + .thumb-cursor:active { + background: ${neutralForegroundRest}; + } + .track-start { + background: ${accentForegroundRest}; + position: absolute; + height: 100%; + left: 0; + border-radius: calc(${controlCornerRadius} * 1px); + } + :host([orientation="horizontal"]) .thumb-container { + transform: translateX(calc(var(--thumb-size) * 0.5px)) translateY(calc(var(--thumb-translate) * 1px)); + } + :host([orientation="vertical"]) .thumb-container { + transform: translateX(calc(var(--thumb-translate) * 1px)) translateY(calc(var(--thumb-size) * 0.5px)); + } + :host([orientation="horizontal"]) { + min-width: calc(var(--thumb-size) * 1px); + } + :host([orientation="horizontal"]) .track { + right: calc(var(--track-overhang) * 1px); + left: calc(var(--track-overhang) * 1px); + align-self: start; + height: calc(var(--track-width) * 1px); + } + :host([orientation="vertical"]) .track { + top: calc(var(--track-overhang) * 1px); + bottom: calc(var(--track-overhang) * 1px); + width: calc(var(--track-width) * 1px); + height: 100%; + } + .track { + background: ${neutralStrokeRest}; + position: absolute; + border-radius: calc(${controlCornerRadius} * 1px); + } + :host([orientation="vertical"]) { + height: calc(var(--fast-slider-height) * 1px); + min-height: calc(var(--thumb-size) * 1px); + min-width: calc(${designUnit} * 20px); + } + :host([orientation="vertical"]) .track-start { + height: auto; + width: 100%; + top: 0; + } + :host([disabled]), :host([readonly]) { + cursor: ${disabledCursor}; + } + :host([disabled]) { + opacity: ${disabledOpacity}; + } + `.withBehaviors( + new DirectionalStyleSheetBehavior(ltr, rtl), + forcedColorsStylesheetBehavior( + css` + .thumb-cursor { + forced-color-adjust: none; + border-color: ${SystemColors.FieldText}; + background: ${SystemColors.FieldText}; + } + .thumb-cursor:hover, + .thumb-cursor:active { + background: ${SystemColors.Highlight}; + } + .track { + forced-color-adjust: none; + background: ${SystemColors.FieldText}; + } + :host(:${focusVisible}) .thumb-cursor { + border-color: ${SystemColors.Highlight}; + } + :host([disabled]) { + opacity: 1; + } + :host([disabled]) .track, + :host([disabled]) .thumb-cursor { + forced-color-adjust: none; + background: ${SystemColors.GrayText}; + } - :host(:${focusVisible}) .thumb-cursor { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.Highlight}; - box-shadow: - 0 0 0 2px ${SystemColors.Field}, - 0 0 0 4px ${SystemColors.FieldText}; - } - `) - ); + :host(:${focusVisible}) .thumb-cursor { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + ` + ) + ); diff --git a/packages/components/src/styles/direction.ts b/packages/components/src/styles/direction.ts new file mode 100644 index 00000000..f8db6ea7 --- /dev/null +++ b/packages/components/src/styles/direction.ts @@ -0,0 +1,102 @@ +import { + Behavior, + ElementStyles, + FASTElement, + Subscriber, +} from "@microsoft/fast-element"; +import { DesignTokenChangeRecord } from "@microsoft/fast-foundation"; +import { Direction } from "@microsoft/fast-web-utilities"; +import { direction as directionDesignToken } from "../design-tokens.js"; +/** + * Behavior to conditionally apply LTR and RTL stylesheets. To determine which to apply, + * the behavior will use the nearest DesignSystemProvider's 'direction' design system value. + * + * @public + * @example + * ```ts + * import { css } from "@microsoft/fast-element"; + * import { DirectionalStyleSheetBehavior } from "@microsoft/fast-foundation"; + * + * css` + * // ... + * `.withBehaviors(new DirectionalStyleSheetBehavior( + * css`:host { content: "ltr"}`), + * css`:host { content: "rtl"}`), + * ) + * ``` + */ +export class DirectionalStyleSheetBehavior implements Behavior { + private ltr: ElementStyles | null; + private rtl: ElementStyles | null; + private cache: WeakMap< + HTMLElement, + DirectionalStyleSheetBehaviorSubscription + > = new WeakMap(); + + constructor(ltr: ElementStyles | null, rtl: ElementStyles | null) { + this.ltr = ltr; + this.rtl = rtl; + } + + /** + * @internal + */ + public bind(source: FASTElement & HTMLElement) { + this.attach(source); + } + + /** + * @internal + */ + public unbind(source: FASTElement & HTMLElement) { + const cache = this.cache.get(source); + + if (cache) { + directionDesignToken.unsubscribe(cache); + } + } + + private attach(source: FASTElement & HTMLElement) { + const subscriber = + this.cache.get(source) || + new DirectionalStyleSheetBehaviorSubscription(this.ltr, this.rtl, source); + + const value = directionDesignToken.getValueFor(source); + directionDesignToken.subscribe(subscriber); + subscriber.attach(value); + + this.cache.set(source, subscriber); + } +} + +/** + * Subscription for {@link DirectionalStyleSheetBehavior} + */ +class DirectionalStyleSheetBehaviorSubscription implements Subscriber { + private attached: ElementStyles | null = null; + + constructor( + private ltr: ElementStyles | null, + private rtl: ElementStyles | null, + private source: HTMLElement & FASTElement + ) {} + + public handleChange({ + target, + token, + }: DesignTokenChangeRecord) { + this.attach(token.getValueFor(target)); + } + + public attach(direction: Direction) { + if (this.attached !== this[direction]) { + if (this.attached !== null) { + this.source.$fastController.removeStyles(this.attached); + } + this.attached = this[direction]; + if (this.attached !== null) { + this.source.$fastController.addStyles(this.attached); + } + } + } +} diff --git a/packages/components/src/styles/elevation.ts b/packages/components/src/styles/elevation.ts index b6d3566c..b319b741 100644 --- a/packages/components/src/styles/elevation.ts +++ b/packages/components/src/styles/elevation.ts @@ -1,7 +1,3 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - /** * Define shadow algorithms. * @@ -12,12 +8,12 @@ * @internal */ export const ambientShadow = - '0 0 calc((var(--elevation) * 0.225px) + 2px) rgba(0, 0, 0, calc(.11 * (2 - var(--background-luminance, 1))))'; + "0 0 calc((var(--elevation) * 0.225px) + 2px) rgba(0, 0, 0, calc(.11 * (2 - var(--background-luminance, 1))))"; /** * @internal */ export const directionalShadow = - '0 calc(var(--elevation) * 0.4px) calc((var(--elevation) * 0.9px)) rgba(0, 0, 0, calc(.13 * (2 - var(--background-luminance, 1))))'; + "0 calc(var(--elevation) * 0.4px) calc((var(--elevation) * 0.9px)) rgba(0, 0, 0, calc(.13 * (2 - var(--background-luminance, 1))))"; /** * Applies the box-shadow CSS rule set to the elevation formula. diff --git a/packages/components/src/styles/index.ts b/packages/components/src/styles/index.ts index 516debab..70a001e8 100644 --- a/packages/components/src/styles/index.ts +++ b/packages/components/src/styles/index.ts @@ -1,6 +1,4 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -export * from './elevation'; -export * from './patterns/index'; -export * from './size'; +export * from "./elevation.js"; +export * from "./patterns/index.js"; +export * from "./size.js"; +export * from "./direction.js"; diff --git a/packages/components/src/styles/patterns/button.ts b/packages/components/src/styles/patterns/button.ts new file mode 100644 index 00000000..e3856477 --- /dev/null +++ b/packages/components/src/styles/patterns/button.ts @@ -0,0 +1,501 @@ +import { css } from "@microsoft/fast-element"; +import { + display, + focusVisible, + forcedColorsStylesheetBehavior, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { heightNumber } from "../size.js"; +import { + accentFillActive, + accentFillHover, + accentFillRest, + accentForegroundActive, + accentForegroundHover, + accentForegroundRest, + bodyFont, + controlCornerRadius, + density, + designUnit, + focusStrokeInner, + focusStrokeOuter, + focusStrokeWidth, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillActive, + neutralFillHover, + neutralFillRest, + neutralFillStealthActive, + neutralFillStealthHover, + neutralFillStealthRest, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../../design-tokens.js"; + +/** + * @internal + */ +export const BaseButtonStyles = css` + ${display("inline-flex")} :host { + font-family: ${bodyFont}; + outline: none; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + height: calc(${heightNumber} * 1px); + min-width: calc(${heightNumber} * 1px); + background-color: ${neutralFillRest}; + color: ${neutralForegroundRest}; + border-radius: calc(${controlCornerRadius} * 1px); + fill: currentcolor; + cursor: pointer; + } + + .control { + background: transparent; + height: inherit; + flex-grow: 1; + box-sizing: border-box; + display: inline-flex; + justify-content: center; + align-items: baseline; + padding: 0 calc((10 + (${designUnit} * 2 * ${density})) * 1px); + white-space: nowrap; + outline: none; + text-decoration: none; + border: calc(${strokeWidth} * 1px) solid transparent; + color: inherit; + border-radius: inherit; + fill: inherit; + cursor: inherit; + font-weight: inherit; + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + :host(:hover) { + background-color: ${neutralFillHover}; + } + + :host(:active) { + background-color: ${neutralFillActive}; + } + + .control:${focusVisible} { + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${focusStrokeOuter} inset; + } + + .control::-moz-focus-inner { + border: 0; + } + + .start, + .content, + .end { + align-self: center; + } + + .start, + .end { + display: flex; + } + + .control.icon-only { + padding: 0; + line-height: 0; + } + + ::slotted(svg) { + ${ + /* Glyph size and margin-left is temporary - + replace when adaptive typography is figured out */ "" + } width: 16px; + height: 16px; + pointer-events: none; + } + + .start { + margin-inline-end: 11px; + } + + .end { + margin-inline-start: 11px; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host .control { + background-color: ${SystemColors.ButtonFace}; + border-color: ${SystemColors.ButtonText}; + color: ${SystemColors.ButtonText}; + fill: currentColor; + } + + :host(:hover) .control { + forced-color-adjust: none; + background-color: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + .control:${focusVisible} { + forced-color-adjust: none; + background-color: ${SystemColors.Highlight}; + border-color: ${SystemColors.ButtonText}; + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${SystemColors.ButtonText} inset; + color: ${SystemColors.HighlightText}; + } + + .control:hover, + :host([appearance="outline"]) .control:hover { + border-color: ${SystemColors.ButtonText}; + } + + :host([href]) .control { + border-color: ${SystemColors.LinkText}; + color: ${SystemColors.LinkText}; + } + + :host([href]) .control:hover, + :host([href]) .control:${focusVisible}{ + forced-color-adjust: none; + background: ${SystemColors.ButtonFace}; + border-color: ${SystemColors.LinkText}; + box-shadow: 0 0 0 1px ${SystemColors.LinkText} inset; + color: ${SystemColors.LinkText}; + fill: currentColor; + } + ` + ) +); + +/** + * @internal + */ +export const AccentButtonStyles = css` + :host([appearance="accent"]) { + background: ${accentFillRest}; + color: ${foregroundOnAccentRest}; + } + + :host([appearance="accent"]:hover) { + background: ${accentFillHover}; + color: ${foregroundOnAccentHover}; + } + + :host([appearance="accent"]:active) .control:active { + background: ${accentFillActive}; + color: ${foregroundOnAccentActive}; + } + + :host([appearance="accent"]) .control:${focusVisible} { + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${focusStrokeOuter} inset, + 0 0 0 calc((${focusStrokeWidth} + ${strokeWidth}) * 1px) ${focusStrokeInner} inset; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="accent"]) .control { + forced-color-adjust: none; + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + } + + :host([appearance="accent"]) .control:hover, + :host([appearance="accent"]:active) .control:active { + background: ${SystemColors.HighlightText}; + border-color: ${SystemColors.Highlight}; + color: ${SystemColors.Highlight}; + } + + :host([appearance="accent"]) .control:${focusVisible} { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${SystemColors.HighlightText} inset; + } + + :host([appearance="accent"][href]) .control{ + background: ${SystemColors.LinkText}; + color: ${SystemColors.HighlightText}; + } + + :host([appearance="accent"][href]) .control:hover { + background: ${SystemColors.ButtonFace}; + border-color: ${SystemColors.LinkText}; + box-shadow: none; + color: ${SystemColors.LinkText}; + fill: currentColor; + } + + :host([appearance="accent"][href]) .control:${focusVisible} { + border-color: ${SystemColors.LinkText}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${SystemColors.HighlightText} inset; + } + ` + ) +); + +/** + * @internal + */ +export const HypertextStyles = css` + :host([appearance="hypertext"]) { + font-size: inherit; + line-height: inherit; + height: auto; + min-width: 0; + background: transparent; + } + + :host([appearance="hypertext"]) .control { + display: inline; + padding: 0; + border: none; + box-shadow: none; + border-radius: 0; + line-height: 1; + } + + :host a.control:not(:link) { + background-color: transparent; + cursor: default; + } + :host([appearance="hypertext"]) .control:link, + :host([appearance="hypertext"]) .control:visited { + background: transparent; + color: ${accentForegroundRest}; + border-bottom: calc(${strokeWidth} * 1px) solid ${accentForegroundRest}; + } + + :host([appearance="hypertext"]:hover), + :host([appearance="hypertext"]) .control:hover { + background: transparent; + border-bottom-color: ${accentForegroundHover}; + } + + :host([appearance="hypertext"]:active), + :host([appearance="hypertext"]) .control:active { + background: transparent; + border-bottom-color: ${accentForegroundActive}; + } + + :host([appearance="hypertext"]) .control:${focusVisible} { + border-bottom: calc(${focusStrokeWidth} * 1px) solid ${focusStrokeOuter}; + margin-bottom: calc(calc(${strokeWidth} - ${focusStrokeWidth}) * 1px); + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="hypertext"]:hover) { + background-color: ${SystemColors.ButtonFace}; + color: ${SystemColors.ButtonText}; + } + :host([appearance="hypertext"][href]) .control:hover, + :host([appearance="hypertext"][href]) .control:active, + :host([appearance="hypertext"][href]) .control:${focusVisible} { + color: ${SystemColors.LinkText}; + border-bottom-color: ${SystemColors.LinkText}; + box-shadow: none; + } + ` + ) +); + +/** + * @internal + */ +export const LightweightButtonStyles = css` + :host([appearance="lightweight"]) { + background: transparent; + color: ${accentForegroundRest}; + } + + :host([appearance="lightweight"]) .control { + padding: 0; + height: initial; + border: none; + box-shadow: none; + border-radius: 0; + } + + :host([appearance="lightweight"]:hover) { + background: transparent; + color: ${accentForegroundHover}; + } + + :host([appearance="lightweight"]:active) { + background: transparent; + color: ${accentForegroundActive}; + } + + :host([appearance="lightweight"]) .content { + position: relative; + } + + :host([appearance="lightweight"]) .content::before { + content: ""; + display: block; + height: calc(${strokeWidth} * 1px); + position: absolute; + top: calc(1em + 4px); + width: 100%; + } + + :host([appearance="lightweight"]:hover) .content::before { + background: ${accentForegroundHover}; + } + + :host([appearance="lightweight"]:active) .content::before { + background: ${accentForegroundActive}; + } + + :host([appearance="lightweight"]) .control:${focusVisible} .content::before { + background: ${neutralForegroundRest}; + height: calc(${focusStrokeWidth} * 1px); + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="lightweight"]) .control:hover, + :host([appearance="lightweight"]) .control:${focusVisible} { + forced-color-adjust: none; + background: ${SystemColors.ButtonFace}; + color: ${SystemColors.Highlight}; + } + :host([appearance="lightweight"]) .control:hover .content::before, + :host([appearance="lightweight"]) .control:${focusVisible} .content::before { + background: ${SystemColors.Highlight}; + } + + :host([appearance="lightweight"][href]) .control:hover, + :host([appearance="lightweight"][href]) .control:${focusVisible} { + background: ${SystemColors.ButtonFace}; + box-shadow: none; + color: ${SystemColors.LinkText}; + } + + :host([appearance="lightweight"][href]) .control:hover .content::before, + :host([appearance="lightweight"][href]) .control:${focusVisible} .content::before { + background: ${SystemColors.LinkText}; + } + ` + ) +); + +/** + * @internal + */ +export const OutlineButtonStyles = css` + :host([appearance="outline"]) { + background: transparent; + border-color: ${accentFillRest}; + } + + :host([appearance="outline"]:hover) { + border-color: ${accentFillHover}; + } + + :host([appearance="outline"]:active) { + border-color: ${accentFillActive}; + } + + :host([appearance="outline"]) .control { + border-color: inherit; + } + + :host([appearance="outline"]) .control:${focusVisible} { + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${focusStrokeOuter} inset; + border-color: ${focusStrokeOuter}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="outline"]) .control { + border-color: ${SystemColors.ButtonText}; + } + :host([appearance="outline"]) .control:${focusVisible} { + forced-color-adjust: none; + background-color: ${SystemColors.Highlight}; + border-color: ${SystemColors.ButtonText}; + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) ${SystemColors.ButtonText} inset; + color: ${SystemColors.HighlightText}; + fill: currentColor; + } + :host([appearance="outline"][href]) .control { + background: ${SystemColors.ButtonFace}; + border-color: ${SystemColors.LinkText}; + color: ${SystemColors.LinkText}; + fill: currentColor; + } + :host([appearance="outline"][href]) .control:hover, + :host([appearance="outline"][href]) .control:${focusVisible} { + forced-color-adjust: none; + border-color: ${SystemColors.LinkText}; + box-shadow: 0 0 0 1px ${SystemColors.LinkText} inset; + } + ` + ) +); + +/** + * @internal + */ +export const StealthButtonStyles = css` + :host([appearance="stealth"]) { + background: ${neutralFillStealthRest}; + } + + :host([appearance="stealth"]:hover) { + background: ${neutralFillStealthHover}; + } + + :host([appearance="stealth"]:active) { + background: ${neutralFillStealthActive}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([appearance="stealth"]), + :host([appearance="stealth"]) .control { + forced-color-adjust: none; + background: ${SystemColors.ButtonFace}; + border-color: transparent; + color: ${SystemColors.ButtonText}; + fill: currentColor; + } + + :host([appearance="stealth"]:hover) .control { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + fill: currentColor; + } + + :host([appearance="stealth"]:${focusVisible}) .control { + background: ${SystemColors.Highlight}; + box-shadow: 0 0 0 1px ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + fill: currentColor; + } + + :host([appearance="stealth"][href]) .control { + color: ${SystemColors.LinkText}; + } + + :host([appearance="stealth"][href]:hover) .control, + :host([appearance="stealth"][href]:${focusVisible}) .control { + background: ${SystemColors.LinkText}; + border-color: ${SystemColors.LinkText}; + color: ${SystemColors.HighlightText}; + fill: currentColor; + } + + :host([appearance="stealth"][href]:${focusVisible}) .control { + forced-color-adjust: none; + box-shadow: 0 0 0 1px ${SystemColors.LinkText}; + } + ` + ) +); diff --git a/packages/components/src/styles/patterns/index.ts b/packages/components/src/styles/patterns/index.ts index edeafbef..1968cbf4 100644 --- a/packages/components/src/styles/patterns/index.ts +++ b/packages/components/src/styles/patterns/index.ts @@ -1,4 +1 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -export * from './field'; +export * from "./button.js"; diff --git a/packages/components/src/styles/size.ts b/packages/components/src/styles/size.ts index 0b97ab4f..2adeff3e 100644 --- a/packages/components/src/styles/size.ts +++ b/packages/components/src/styles/size.ts @@ -1,9 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { cssPartial } from '@microsoft/fast-element'; -import { baseHeightMultiplier, density, designUnit } from '../design-tokens'; +import { cssPartial } from "@microsoft/fast-element"; +import { baseHeightMultiplier, density, designUnit } from "../design-tokens.js"; /** * A formula to retrieve the control height. diff --git a/packages/components/src/switch/index.ts b/packages/components/src/switch/index.ts index a108283d..e9cef3fd 100644 --- a/packages/components/src/switch/index.ts +++ b/packages/components/src/switch/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - Switch, - SwitchOptions, - switchTemplate as template -} from '@microsoft/fast-foundation'; -import { switchStyles as styles } from './switch.styles'; + Switch, + SwitchOptions, + switchTemplate as template, +} from "@microsoft/fast-foundation"; +import { switchStyles as styles } from "./switch.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Switch} registration for configuring the component with a DesignSystem. @@ -15,15 +12,15 @@ import { switchStyles as styles } from './switch.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpSwitch = Switch.compose({ - baseName: 'switch', - template, - styles, - switch: /* html */ ` - - ` +export const fastSwitch = Switch.compose({ + baseName: "switch", + template, + styles, + switch: /* html */ ` + + `, }); /** diff --git a/packages/components/src/switch/switch.stories.ts b/packages/components/src/switch/switch.stories.ts index 15115123..32672fdc 100644 --- a/packages/components/src/switch/switch.stories.ts +++ b/packages/components/src/switch/switch.stories.ts @@ -1,91 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; -import { Switch } from './index'; +import Examples from "./fixtures/base.html"; +import "./index.js"; export default { - title: 'Components/Switch', - argTypes: { - label: { control: 'text' }, - withMessages: { control: 'boolean' }, - isChecked: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isReadOnly: { control: 'boolean' }, - onChange: { - action: 'changed', - table: { - disable: true - } - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.label} - ${ - args.withMessages - ? `On - Off` - : '' - } - ` - ); - - const switch_ = container.firstChild as Switch; - - if (args.onChange) { - switch_.addEventListener('change', args.onChange); - } - - return switch_; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Feature', - isChecked: false, - isDisabled: false, - isReadOnly: false, - withMessages: false, - onChange: action('switch-onchange') + title: "Switch", }; -export const WithChecked: StoryObj = { render: Template.bind({}) }; -WithChecked.args = { - ...Default.args, - isChecked: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - isDisabled: true -}; - -export const WithReadOnly: StoryObj = { render: Template.bind({}) }; -WithReadOnly.args = { - ...Default.args, - isReadOnly: true -}; - -export const WithMessages: StoryObj = { render: Template.bind({}) }; -WithMessages.args = { - ...Default.args, - withMessages: true -}; +export const Switch = () => Examples; diff --git a/packages/components/src/switch/switch.styles.ts b/packages/components/src/switch/switch.styles.ts index 00a1660c..e3426f49 100644 --- a/packages/components/src/switch/switch.styles.ts +++ b/packages/components/src/switch/switch.styles.ts @@ -1,281 +1,267 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - SwitchOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + SwitchOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillActive, - accentFillFocus, - accentFillHover, - accentFillRest, - bodyFont, - controlCornerRadius, - designUnit, - DirectionalStyleSheetBehavior, - disabledOpacity, - focusStrokeWidth, - foregroundOnAccentActive, - foregroundOnAccentHover, - foregroundOnAccentRest, - neutralFillInputActive, - neutralFillInputHover, - neutralFillInputRest, - neutralForegroundRest, - neutralStrokeActive, - neutralStrokeHover, - neutralStrokeRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + fillColor, + focusStrokeOuter, + foregroundOnAccentActive, + foregroundOnAccentHover, + foregroundOnAccentRest, + neutralFillInputActive, + neutralFillInputHover, + neutralFillInputRest, + neutralForegroundRest, + neutralStrokeActive, + neutralStrokeHover, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { DirectionalStyleSheetBehavior, heightNumber } from "../styles/index.js"; /** * Styles for Switch * @public */ -export const switchStyles: FoundationElementTemplate< - ElementStyles, - SwitchOptions -> = (context, definition) => - css` - :host([hidden]) { - display: none; - } - - ${display('inline-flex')} :host { - align-items: center; - outline: none; - font-family: ${bodyFont}; - margin: calc(${designUnit} * 1px) 0; - ${ - /* - * Chromium likes to select label text or the default slot when - * the checkbox is clicked. Maybe there is a better solution here? - */ '' - } user-select: none; - } - - :host([disabled]) { - opacity: ${disabledOpacity}; - } - - :host([disabled]) .label, - :host([readonly]) .label, - :host([readonly]) .switch, - :host([disabled]) .switch { - cursor: ${disabledCursor}; - } - - .switch { - position: relative; - outline: none; - box-sizing: border-box; - width: calc(${heightNumber} * 1px); - height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); - background: ${neutralFillInputRest}; - border-radius: calc(${controlCornerRadius} * 1px); - border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; - } +export const switchStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + :host([hidden]) { + display: none; + } - .switch:hover { - background: ${neutralFillInputHover}; - border-color: ${neutralStrokeHover}; - cursor: pointer; - } + ${display("inline-flex")} :host { + align-items: center; + outline: none; + font-family: ${bodyFont}; + margin: calc(${designUnit} * 1px) 0; + ${ + /* + * Chromium likes to select label text or the default slot when + * the checkbox is clicked. Maybe there is a better solution here? + */ "" + } user-select: none; + } - host([disabled]) .switch:hover, - host([readonly]) .switch:hover { - background: ${neutralFillInputHover}; - border-color: ${neutralStrokeHover}; - cursor: ${disabledCursor}; - } + :host([disabled]) { + opacity: ${disabledOpacity}; + } - :host(:not([disabled])) .switch:active { - background: ${neutralFillInputActive}; - border-color: ${neutralStrokeActive}; - } + :host([disabled]) .label, + :host([readonly]) .label, + :host([readonly]) .switch, + :host([disabled]) .switch { + cursor: ${disabledCursor}; + } - :host(:${focusVisible}) .switch { - outline-offset: 2px; - outline: solid calc(${focusStrokeWidth} * 1px) ${accentFillFocus}; - } + .switch { + position: relative; + outline: none; + box-sizing: border-box; + width: calc(${heightNumber} * 1px); + height: calc((${heightNumber} / 2 + ${designUnit}) * 1px); + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${neutralStrokeRest}; + } - .checked-indicator { - position: absolute; - top: 5px; - bottom: 5px; - background: ${neutralForegroundRest}; - border-radius: calc(${controlCornerRadius} * 1px); - transition: all 0.2s ease-in-out; - } + .switch:hover { + background: ${neutralFillInputHover}; + border-color: ${neutralStrokeHover}; + cursor: pointer; + } - .status-message { - color: ${neutralForegroundRest}; - cursor: pointer; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - } + host([disabled]) .switch:hover, + host([readonly]) .switch:hover { + background: ${neutralFillInputHover}; + border-color: ${neutralStrokeHover}; + cursor: ${disabledCursor}; + } - :host([disabled]) .status-message, - :host([readonly]) .status-message { - cursor: ${disabledCursor}; - } + :host(:not([disabled])) .switch:active { + background: ${neutralFillInputActive}; + border-color: ${neutralStrokeActive}; + } - .label { - color: ${neutralForegroundRest}; + :host(:${focusVisible}) .switch { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } - ${ - /* Need to discuss with Brian how HorizontalSpacingNumber can work. https://github.com/microsoft/fast/issues/2766 */ '' - } margin-inline-end: calc(${designUnit} * 2px + 2px); - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - cursor: pointer; - } + .checked-indicator { + position: absolute; + top: 5px; + bottom: 5px; + background: ${neutralForegroundRest}; + border-radius: calc(${controlCornerRadius} * 1px); + transition: all 0.2s ease-in-out; + } - .label__hidden { - display: none; - visibility: hidden; - } + .status-message { + color: ${neutralForegroundRest}; + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + } - ::slotted([slot='checked-message']), - ::slotted([slot='unchecked-message']) { - margin-inline-start: calc(${designUnit} * 2px + 2px); - } + :host([disabled]) .status-message, + :host([readonly]) .status-message { + cursor: ${disabledCursor}; + } - :host([aria-checked='true']) .checked-indicator { - background: ${foregroundOnAccentRest}; - } + .label { + color: ${neutralForegroundRest}; + margin-inline-end: calc(${designUnit} * 2px + 2px); + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + cursor: pointer; + } - :host([aria-checked='true']) .switch { - background: ${accentFillRest}; - border-color: ${accentFillRest}; - } + .label__hidden { + display: none; + visibility: hidden; + } - :host([aria-checked='true']:not([disabled])) .switch:hover { - background: ${accentFillHover}; - border-color: ${accentFillHover}; - } + ::slotted([slot="checked-message"]), + ::slotted([slot="unchecked-message"]) { + margin-inline-start: calc(${designUnit} * 2px + 2px); + } - :host([aria-checked='true']:not([disabled])) - .switch:hover - .checked-indicator { - background: ${foregroundOnAccentHover}; - } + :host([aria-checked="true"]) .checked-indicator { + background: ${foregroundOnAccentRest}; + } - :host([aria-checked='true']:not([disabled])) .switch:active { - background: ${accentFillActive}; - border-color: ${accentFillActive}; - } + :host([aria-checked="true"]) .switch { + background: ${accentFillRest}; + border-color: ${accentFillRest}; + } - :host([aria-checked='true']:not([disabled])) - .switch:active - .checked-indicator { - background: ${foregroundOnAccentActive}; - } + :host([aria-checked="true"]:not([disabled])) .switch:hover { + background: ${accentFillHover}; + border-color: ${accentFillHover}; + } - :host([aria-checked="true"]:${focusVisible}:not([disabled])) .switch { - outline: solid calc(${focusStrokeWidth} * 1px) ${accentFillFocus}; - } + :host([aria-checked="true"]:not([disabled])) .switch:hover .checked-indicator { + background: ${foregroundOnAccentHover}; + } - .unchecked-message { - display: block; - } + :host([aria-checked="true"]:not([disabled])) .switch:active { + background: ${accentFillActive}; + border-color: ${accentFillActive}; + } - .checked-message { - display: none; - } + :host([aria-checked="true"]:not([disabled])) .switch:active .checked-indicator { + background: ${foregroundOnAccentActive}; + } - :host([aria-checked='true']) .unchecked-message { - display: none; - } + :host([aria-checked="true"]:${focusVisible}:not([disabled])) .switch { + box-shadow: 0 0 0 2px ${fillColor}, 0 0 0 4px ${focusStrokeOuter}; + } - :host([aria-checked='true']) .checked-message { - display: block; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - .checked-indicator, - :host(:not([disabled])) .switch:active .checked-indicator { - forced-color-adjust: none; - background: ${SystemColors.FieldText}; - } - .switch { - forced-color-adjust: none; - background: ${SystemColors.Field}; - border-color: ${SystemColors.FieldText}; - } - :host(:not([disabled])) .switch:hover { - background: ${SystemColors.HighlightText}; - border-color: ${SystemColors.Highlight}; - } - :host([aria-checked='true']) .switch { - background: ${SystemColors.Highlight}; - border-color: ${SystemColors.Highlight}; - } - :host([aria-checked='true']:not([disabled])) .switch:hover, - :host(:not([disabled])) .switch:active { - background: ${SystemColors.HighlightText}; - border-color: ${SystemColors.Highlight}; - } - :host([aria-checked='true']) .checked-indicator { - background: ${SystemColors.HighlightText}; - } - :host([aria-checked='true']:not([disabled])) - .switch:hover - .checked-indicator { - background: ${SystemColors.Highlight}; - } - :host([disabled]) { - opacity: 1; - } - :host(:${focusVisible}) .switch { - border-color: ${SystemColors.Highlight}; - outline-offset: 2px; - outline: solid calc(${focusStrokeWidth} * 1px) ${SystemColors.FieldText}; - } - :host([aria-checked="true"]:${focusVisible}:not([disabled])) .switch { - outline: solid calc(${focusStrokeWidth} * 1px) ${SystemColors.FieldText}; - } - :host([disabled]) .checked-indicator { - background: ${SystemColors.GrayText}; - } - :host([disabled]) .switch { - background: ${SystemColors.Field}; - border-color: ${SystemColors.GrayText}; - } - `), - new DirectionalStyleSheetBehavior( - css` - .checked-indicator { - left: 5px; - right: calc(((${heightNumber} / 2) + 1) * 1px); + .unchecked-message { + display: block; } - :host([aria-checked='true']) .checked-indicator { - left: calc(((${heightNumber} / 2) + 1) * 1px); - right: 5px; + .checked-message { + display: none; } - `, - css` - .checked-indicator { - right: 5px; - left: calc(((${heightNumber} / 2) + 1) * 1px); + + :host([aria-checked="true"]) .unchecked-message { + display: none; } - :host([aria-checked='true']) .checked-indicator { - right: calc(((${heightNumber} / 2) + 1) * 1px); - left: 5px; + :host([aria-checked="true"]) .checked-message { + display: block; } - ` - ) - ); + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .checked-indicator, + :host(:not([disabled])) .switch:active .checked-indicator { + forced-color-adjust: none; + background: ${SystemColors.FieldText}; + } + .switch { + forced-color-adjust: none; + background: ${SystemColors.Field}; + border-color: ${SystemColors.FieldText}; + } + :host(:not([disabled])) .switch:hover { + background: ${SystemColors.HighlightText}; + border-color: ${SystemColors.Highlight}; + } + :host([aria-checked="true"]) .switch { + background: ${SystemColors.Highlight}; + border-color: ${SystemColors.Highlight}; + } + :host([aria-checked="true"]:not([disabled])) .switch:hover, + :host(:not([disabled])) .switch:active { + background: ${SystemColors.HighlightText}; + border-color: ${SystemColors.Highlight}; + } + :host([aria-checked="true"]) .checked-indicator { + background: ${SystemColors.HighlightText}; + } + :host([aria-checked="true"]:not([disabled])) .switch:hover .checked-indicator { + background: ${SystemColors.Highlight}; + } + :host([disabled]) { + opacity: 1; + } + :host(:${focusVisible}) .switch { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + :host([aria-checked="true"]:${focusVisible}:not([disabled])) .switch { + box-shadow: 0 0 0 2px ${SystemColors.Field}, 0 0 0 4px ${SystemColors.FieldText}; + } + :host([disabled]) .checked-indicator { + background: ${SystemColors.GrayText}; + } + :host([disabled]) .switch { + background: ${SystemColors.Field}; + border-color: ${SystemColors.GrayText}; + } + ` + ), + new DirectionalStyleSheetBehavior( + css` + .checked-indicator { + left: 5px; + right: calc(((${heightNumber} / 2) + 1) * 1px); + } + + :host([aria-checked="true"]) .checked-indicator { + left: calc(((${heightNumber} / 2) + 1) * 1px); + right: 5px; + } + `, + css` + .checked-indicator { + right: 5px; + left: calc(((${heightNumber} / 2) + 1) * 1px); + } + + :host([aria-checked="true"]) .checked-indicator { + right: calc(((${heightNumber} / 2) + 1) * 1px); + left: 5px; + } + ` + ) + ); diff --git a/packages/components/src/tab-panel/index.ts b/packages/components/src/tab-panel/index.ts index a476ba34..32e220b2 100644 --- a/packages/components/src/tab-panel/index.ts +++ b/packages/components/src/tab-panel/index.ts @@ -1,11 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - TabPanel, - tabPanelTemplate as template -} from '@microsoft/fast-foundation'; -import { tabPanelStyles as styles } from '@microsoft/fast-components'; +import { TabPanel, tabPanelTemplate as template } from "@microsoft/fast-foundation"; +import { tabPanelStyles as styles } from "./tab-panel.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#TabPanel} registration for configuring the component with a DesignSystem. @@ -14,12 +8,12 @@ import { tabPanelStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpTabPanel = TabPanel.compose({ - baseName: 'tab-panel', - template, - styles +export const fastTabPanel = TabPanel.compose({ + baseName: "tab-panel", + template, + styles, }); /** diff --git a/packages/components/src/tab-panel/tab-panel.styles.ts b/packages/components/src/tab-panel/tab-panel.styles.ts new file mode 100644 index 00000000..0152c9ee --- /dev/null +++ b/packages/components/src/tab-panel/tab-panel.styles.ts @@ -0,0 +1,24 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { display, FoundationElementTemplate } from "@microsoft/fast-foundation"; +import { + density, + designUnit, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; + +/** + * Styles for Tab Panel + * @public + */ +export const tabPanelStyles: FoundationElementTemplate = ( + context, + definition +) => css` + ${display("block")} :host { + box-sizing: border-box; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + padding: 0 calc((6 + (${designUnit} * 2 * ${density})) * 1px); + } +`; diff --git a/packages/components/src/tab/index.ts b/packages/components/src/tab/index.ts index c946ab05..d541e607 100644 --- a/packages/components/src/tab/index.ts +++ b/packages/components/src/tab/index.ts @@ -1,8 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { Tab, tabTemplate as template } from '@microsoft/fast-foundation'; -import { tabStyles as styles } from './tab.styles'; +import { Tab, tabTemplate as template } from "@microsoft/fast-foundation"; +import { tabStyles as styles } from "./tab.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Tab} registration for configuring the component with a DesignSystem. @@ -11,12 +8,12 @@ import { tabStyles as styles } from './tab.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpTab = Tab.compose({ - baseName: 'tab', - template, - styles +export const fastTab = Tab.compose({ + baseName: "tab", + template, + styles, }); /** diff --git a/packages/components/src/tab/tab.styles.ts b/packages/components/src/tab/tab.styles.ts index 318a3984..ccf8bc48 100644 --- a/packages/components/src/tab/tab.styles.ts +++ b/packages/components/src/tab/tab.styles.ts @@ -1,161 +1,159 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - +import { css, ElementStyles } from "@microsoft/fast-element"; import { - accentFillFocus, - bodyFont, - controlCornerRadius, - designUnit, - disabledOpacity, - focusStrokeWidth, - neutralFillActive, - neutralFillHover, - neutralFillRest, - neutralFillStealthRest, - neutralForegroundHint, - neutralForegroundRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '@microsoft/fast-components'; -import { css, ElementStyles } from '@microsoft/fast-element'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; -import { heightNumber } from '../styles'; + accentForegroundActive, + accentForegroundHover, + accentForegroundRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeOuter, + focusStrokeWidth, + neutralFillActive, + neutralFillHover, + neutralFillRest, + neutralFillStealthRest, + neutralForegroundHint, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/size.js"; /** * Styles for Tab * @public */ export const tabStyles: FoundationElementTemplate = ( - context, - definition + context, + definition ) => - css` - ${display('inline-flex')} :host { - box-sizing: border-box; - font-family: ${bodyFont}; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - height: calc(${heightNumber} * 1px); - padding: calc(${designUnit} * 5px) calc(${designUnit} * 4px); - color: ${neutralForegroundHint}; - fill: currentcolor; - border-radius: 0 0 calc(${controlCornerRadius} * 1px) - calc(${controlCornerRadius} * 1px); - border: calc(${strokeWidth} * 1px) solid transparent; - align-items: center; - grid-row: 2; - justify-content: center; - cursor: pointer; + css` + ${display("inline-flex")} :host { + box-sizing: border-box; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + height: calc(${heightNumber} * 1px); + padding: calc(${designUnit} * 5px) calc(${designUnit} * 4px); + color: ${neutralForegroundHint}; + fill: currentcolor; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid transparent; + align-items: center; + justify-content: center; + grid-row: 1; + cursor: pointer; } :host(:hover) { - color: ${neutralForegroundRest}; - fill: currentcolor; + color: ${neutralForegroundRest}; + fill: currentcolor; } :host(:active) { - color: ${neutralForegroundRest}; - fill: currentcolor; + color: ${neutralForegroundRest}; + fill: currentcolor; } :host([disabled]) { - cursor: ${disabledCursor}; - opacity: ${disabledOpacity}; + cursor: ${disabledCursor}; + opacity: ${disabledOpacity}; } :host([disabled]:hover) { - color: ${neutralForegroundHint}; - background: ${neutralFillStealthRest}; + color: ${neutralForegroundHint}; + background: ${neutralFillStealthRest}; } - :host([aria-selected='true']) { - background: ${neutralFillRest}; - color: ${neutralForegroundRest}; - fill: currentcolor; + :host([aria-selected="true"]) { + background: ${neutralFillRest}; + color: ${accentForegroundRest}; + fill: currentcolor; } - :host([aria-selected='true']:hover) { - background: ${neutralFillHover}; - color: ${neutralForegroundRest}; - fill: currentcolor; + :host([aria-selected="true"]:hover) { + background: ${neutralFillHover}; + color: ${accentForegroundHover}; + fill: currentcolor; } - :host([aria-selected='true']:active) { - background: ${neutralFillActive}; - color: ${neutralForegroundRest}; - fill: currentcolor; + :host([aria-selected="true"]:active) { + background: ${neutralFillActive}; + color: ${accentForegroundActive}; + fill: currentcolor; } :host(:${focusVisible}) { - outline: none; - border-color: ${accentFillFocus}; - box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) - ${accentFillFocus}; + outline: none; + border: calc(${strokeWidth} * 1px) solid ${focusStrokeOuter}; + box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) + ${focusStrokeOuter}; } :host(:focus) { - outline: none; + outline: none; } :host(.vertical) { - justify-content: end; - grid-column: 2; - border-bottom-left-radius: 0; - border-top-right-radius: calc(${controlCornerRadius} * 1px); + justify-content: end; + grid-column: 2; } - :host(.vertical[aria-selected='true']) { - z-index: 2; + :host(.vertical[aria-selected="true"]) { + z-index: 2; } :host(.vertical:hover) { - color: ${neutralForegroundRest}; + color: ${neutralForegroundRest}; } :host(.vertical:active) { - color: ${neutralForegroundRest}; + color: ${neutralForegroundRest}; } - :host(.vertical:hover[aria-selected='true']) { + :host(.vertical:hover[aria-selected="true"]) { } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host { - forced-color-adjust: none; - border-color: transparent; - color: ${SystemColors.ButtonText}; - fill: currentcolor; - } - :host(:hover), - :host(.vertical:hover), - :host([aria-selected='true']:hover) { - background: ${SystemColors.Highlight}; - color: ${SystemColors.HighlightText}; - fill: currentcolor; - } - :host([aria-selected='true']) { - background: ${SystemColors.HighlightText}; - color: ${SystemColors.Highlight}; - fill: currentcolor; - } - :host(:${focusVisible}) { - border-color: ${SystemColors.ButtonText}; - box-shadow: none; - } - :host([disabled]), - :host([disabled]:hover) { - opacity: 1; - color: ${SystemColors.GrayText}; - background: ${SystemColors.ButtonFace}; - } - `) - ); +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host { + forced-color-adjust: none; + border-color: transparent; + color: ${SystemColors.ButtonText}; + fill: currentcolor; + } + :host(:hover), + :host(.vertical:hover), + :host([aria-selected="true"]:hover) { + background: ${SystemColors.Highlight}; + color: ${SystemColors.HighlightText}; + fill: currentcolor; + } + :host([aria-selected="true"]) { + background: ${SystemColors.HighlightText}; + color: ${SystemColors.Highlight}; + fill: currentcolor; + } + :host(:${focusVisible}) { + border-color: ${SystemColors.ButtonText}; + box-shadow: none; + } + :host([disabled]), + :host([disabled]:hover) { + opacity: 1; + color: ${SystemColors.GrayText}; + background: ${SystemColors.ButtonFace}; + } + ` + ) + ); diff --git a/packages/components/src/tabs/index.ts b/packages/components/src/tabs/index.ts index 50322c2b..88b92071 100644 --- a/packages/components/src/tabs/index.ts +++ b/packages/components/src/tabs/index.ts @@ -1,8 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { Tabs, tabsTemplate as template } from '@microsoft/fast-foundation'; -import { tabsStyles as styles } from './tabs.styles'; +import { Tabs, tabsTemplate as template } from "@microsoft/fast-foundation"; +import { tabsStyles as styles } from "./tabs.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Tabs} registration for configuring the component with a DesignSystem. @@ -11,16 +8,16 @@ import { tabsStyles as styles } from './tabs.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpTabs = Tabs.compose({ - baseName: 'tabs', - template, - styles +export const fastTabs = Tabs.compose({ + baseName: "tabs", + template, + styles, }); -export * from '../tab'; -export * from '../tab-panel'; +export * from "../tab/index.js"; +export * from "../tab-panel/index.js"; /** * Base class for Tabs diff --git a/packages/components/src/tabs/tabs.stories.ts b/packages/components/src/tabs/tabs.stories.ts index c7630987..a97dbef8 100644 --- a/packages/components/src/tabs/tabs.stories.ts +++ b/packages/components/src/tabs/tabs.stories.ts @@ -1,74 +1,30 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; +import { STORY_RENDERED } from "@storybook/core-events"; +import addons from "@storybook/addons"; +import Examples from "./fixtures/base.html"; + +function addItem(): void { + const tabsElement = document.getElementById("addTabsExample"); + + if (tabsElement?.children !== undefined) { + const tab: any = document.createElement("fast-tab"); + const tabPanel: any = document.createElement("fast-tab-panel"); + tab.textContent = "Added tab"; + tabPanel.textContent = "Added panel"; + + tabsElement?.appendChild(tab); + tabsElement.insertBefore(tabPanel, tab); + } +} + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase() === "tabs--tabs") { + const button = document.getElementById("add-button") as HTMLElement; + button.addEventListener("click", () => addItem()); + } +}); export default { - title: 'Components/Tabs', - argTypes: { - activePanel: { control: 'select', options: [null, 'One', 'Two', 'Three'] }, - activeIndicator: { control: 'boolean' }, - orientation: { control: 'radio', options: ['horizontal', 'vertical'] } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - Tab one - Tab two - Tab three - - Tab one content. This is for testing. - - - Tab two content. This is for testing. - - - Tab three content. This is for testing. - - ` - ); - - const tabs = container.firstChild as HTMLElement; - - if (args.onChange) { - tabs.addEventListener('change', args.onChange); - } - - return tabs; + title: "Tabs", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - activePanel: null, - activeIndicator: true, - orientation: 'horizontal', - onChange: action('tabs-onchange') -}; - -export const Vertical: StoryObj = { render: Template.bind({}) }; -Vertical.args = { - ...Default.args, - orientation: 'vertical' -}; - -export const WithoutIndicator: StoryObj = { render: Template.bind({}) }; -WithoutIndicator.args = { - ...Default.args, - activeIndicator: false -}; +export const Tabs = () => Examples; diff --git a/packages/components/src/tabs/tabs.styles.ts b/packages/components/src/tabs/tabs.styles.ts index c4fb2ec8..3bf7d930 100644 --- a/packages/components/src/tabs/tabs.styles.ts +++ b/packages/components/src/tabs/tabs.styles.ts @@ -1,134 +1,134 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - display, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - TabsOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + display, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + TabsOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillRest, - bodyFont, - controlCornerRadius, - designUnit, - neutralForegroundRest, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + neutralForegroundRest, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Tabs * @public */ -export const tabsStyles: FoundationElementTemplate< - ElementStyles, - TabsOptions -> = (context, definition) => - css` - ${display('grid')} :host { - box-sizing: border-box; - font-family: ${bodyFont}; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - color: ${neutralForegroundRest}; - grid-template-columns: auto 1fr auto; - grid-template-rows: auto 1fr; - } +export const tabsStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("grid")} :host { + box-sizing: border-box; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + color: ${neutralForegroundRest}; + grid-template-columns: auto 1fr auto; + grid-template-rows: auto 1fr; + } - .tablist { - display: grid; - grid-template-rows: auto auto; - grid-template-columns: auto; - position: relative; - width: max-content; - align-self: end; - padding: calc(${designUnit} * 4px) calc(${designUnit} * 4px) 0; - box-sizing: border-box; - } + .tablist { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: auto; + position: relative; + width: max-content; + align-self: end; + padding: calc(${designUnit} * 4px) calc(${designUnit} * 4px) 0; + box-sizing: border-box; + } - .start, - .end { - align-self: center; - } + .start, + .end { + align-self: center; + } - .activeIndicator { - grid-row: 1; - grid-column: 1; - width: 100%; - height: 4px; - justify-self: center; - background: ${accentFillRest}; - margin-top: 0; - border-radius: calc(${controlCornerRadius} * 1px) - calc(${controlCornerRadius} * 1px) 0 0; - } + .activeIndicator { + grid-row: 2; + grid-column: 1; + width: 100%; + height: 5px; + justify-self: center; + background: ${accentFillRest}; + margin-top: 10px; + border-radius: calc(${controlCornerRadius} * 1px) + calc(${controlCornerRadius} * 1px) 0 0; + } - .activeIndicatorTransition { - transition: transform 0.01s ease-in-out; - } + .activeIndicatorTransition { + transition: transform 0.2s ease-in-out; + } - .tabpanel { - grid-row: 2; - grid-column-start: 1; - grid-column-end: 4; - position: relative; - } + .tabpanel { + grid-row: 2; + grid-column-start: 1; + grid-column-end: 4; + position: relative; + } - :host([orientation='vertical']) { - grid-template-rows: auto 1fr auto; - grid-template-columns: auto 1fr; - } + :host([orientation="vertical"]) { + grid-template-rows: auto 1fr auto; + grid-template-columns: auto 1fr; + } - :host([orientation='vertical']) .tablist { - grid-row-start: 2; - grid-row-end: 2; - display: grid; - grid-template-rows: auto; - grid-template-columns: auto 1fr; - position: relative; - width: max-content; - justify-self: end; - align-self: flex-start; - width: 100%; - padding: 0 calc(${designUnit} * 4px) - calc((${heightNumber} - ${designUnit}) * 1px) 0; - } + :host([orientation="vertical"]) .tablist { + grid-row-start: 2; + grid-row-end: 2; + display: grid; + grid-template-rows: auto; + grid-template-columns: auto 1fr; + position: relative; + width: max-content; + justify-self: end; + align-self: flex-start; + width: 100%; + padding: 0 calc(${designUnit} * 4px) + calc((${heightNumber} - ${designUnit}) * 1px) 0; + } - :host([orientation='vertical']) .tabpanel { - grid-column: 2; - grid-row-start: 1; - grid-row-end: 4; - } + :host([orientation="vertical"]) .tabpanel { + grid-column: 2; + grid-row-start: 1; + grid-row-end: 4; + } - :host([orientation='vertical']) .end { - grid-row: 3; - } + :host([orientation="vertical"]) .end { + grid-row: 3; + } - :host([orientation='vertical']) .activeIndicator { - grid-column: 1; - grid-row: 1; - width: 4px; - height: 100%; - margin-inline-end: 0px; - align-self: center; - border-radius: calc(${controlCornerRadius} * 1px) 0 0 - calc(${controlCornerRadius} * 1px); - } + :host([orientation="vertical"]) .activeIndicator { + grid-column: 1; + grid-row: 1; + width: 5px; + height: 100%; + margin-inline-end: 10px; + align-self: center; + background: ${accentFillRest}; + margin-top: 0; + border-radius: 0 calc(${controlCornerRadius} * 1px) + calc(${controlCornerRadius} * 1px) 0; + } - :host([orientation='vertical']) .activeIndicatorTransition { - transition: transform 0.01s ease-in-out; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - .activeIndicator, - :host([orientation='vertical']) .activeIndicator { - forced-color-adjust: none; - background: ${SystemColors.Highlight}; - } - `) - ); + :host([orientation="vertical"]) .activeIndicatorTransition { + transition: transform 0.2s linear; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .activeIndicator, + :host([orientation="vertical"]) .activeIndicator { + forced-color-adjust: none; + background: ${SystemColors.Highlight}; + } + ` + ) + ); diff --git a/packages/components/src/text-area/index.ts b/packages/components/src/text-area/index.ts index f97ee894..f9aad0d3 100644 --- a/packages/components/src/text-area/index.ts +++ b/packages/components/src/text-area/index.ts @@ -1,12 +1,30 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - +import { attr } from "@microsoft/fast-element"; import { - TextArea as FoundationTextArea, - textAreaTemplate as template -} from '@microsoft/fast-foundation'; -import { TextArea, TextAreaAppearance } from '@microsoft/fast-components'; -import { textAreaStyles as styles } from './text-area.styles'; + TextArea as FoundationTextArea, + textAreaTemplate as template, +} from "@microsoft/fast-foundation"; +import { textAreaStyles as styles } from "./text-area.styles.js"; + +/** + * Text area appearances + * @public + */ +export type TextAreaAppearance = "filled" | "outline"; + +/** + * @internal + */ +export class TextArea extends FoundationTextArea { + /** + * The appearance of the element. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: TextAreaAppearance = "outline"; +} /** * A function that returns a {@link @microsoft/fast-foundation#TextArea} registration for configuring the component with a DesignSystem. @@ -15,18 +33,18 @@ import { textAreaStyles as styles } from './text-area.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus} */ -export const jpTextArea = TextArea.compose({ - baseName: 'text-area', - baseClass: FoundationTextArea, - template, - styles, - shadowOptions: { - delegatesFocus: true - } +export const fastTextArea = TextArea.compose({ + baseName: "text-area", + baseClass: FoundationTextArea, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, }); -export { TextArea, TextAreaAppearance, styles as textAreaStyles }; +export { styles as textAreaStyles }; diff --git a/packages/components/src/text-area/text-area.stories.ts b/packages/components/src/text-area/text-area.stories.ts index 3e169448..f14df2fd 100644 --- a/packages/components/src/text-area/text-area.stories.ts +++ b/packages/components/src/text-area/text-area.stories.ts @@ -1,111 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; -import { TextArea } from './index'; +import TextAreaTemplate from "./fixtures/text-area.html"; +import "./index.js"; export default { - title: 'Components/Text Area', - argTypes: { - label: { control: 'text' }, - placeholder: { control: 'text' }, - value: { control: 'text' }, - maxLength: { control: 'number' }, - isReadOnly: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isAutoFocused: { control: 'boolean' }, - appearance: { control: 'radio', options: ['outline', 'filled'] }, - resize: { - control: 'select', - options: ['none', 'both', 'vertical', 'horizontal'] - }, - onChange: { - action: 'changed', - table: { - disable: true - } - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.label} - ` - ); - - const textField = container.firstChild as TextArea; - - if (args.value) { - textField.value = args.value; - } - - if (args.onChange) { - textField.addEventListener('change', args.onChange); - } - - return textField; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Text Area Label', - placeholder: '', - value: '', - maxLength: '', - resize: 'none', - isReadOnly: false, - isDisabled: false, - isAutoFocused: false, - appearance: 'outline', - onChange: action('text-area-onchange') + title: "Text Area", }; -export const WithPlaceholder: StoryObj = { render: Template.bind({}) }; -WithPlaceholder.args = { - ...Default.args, - placeholder: 'Placeholder Text' -}; - -export const WithAutofocus: StoryObj = { render: Template.bind({}) }; -WithAutofocus.args = { - ...Default.args, - autofocus: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - disabled: true -}; - -export const WithMaxLength: StoryObj = { render: Template.bind({}) }; -WithMaxLength.args = { - ...Default.args, - placeholder: 'This text field can only contain a maximum of 10 characters', - maxLength: 10 -}; - -export const WithReadonly: StoryObj = { render: Template.bind({}) }; -WithReadonly.args = { - ...Default.args, - readonly: true -}; +export const TextArea = () => TextAreaTemplate; diff --git a/packages/components/src/text-area/text-area.styles.ts b/packages/components/src/text-area/text-area.styles.ts index 6df39686..4dc9d742 100644 --- a/packages/components/src/text-area/text-area.styles.ts +++ b/packages/components/src/text-area/text-area.styles.ts @@ -1,142 +1,146 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - +import { css, ElementStyles } from "@microsoft/fast-element"; import { - accentFillFocus, - bodyFont, - controlCornerRadius, - designUnit, - disabledOpacity, - focusStrokeWidth, - neutralFillHover, - neutralFillInputActive, - neutralFillInputHover, - neutralFillInputRest, - neutralFillRest, - neutralFillStrongActive, - neutralFillStrongHover, - neutralFillStrongRest, - neutralForegroundRest, - neutralStrokeRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { css, ElementStyles } from '@microsoft/fast-element'; + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, +} from "@microsoft/fast-foundation"; import { - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate -} from '@microsoft/fast-foundation'; -import { heightNumber } from '../styles/index'; + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeOuter, + neutralFillHover, + neutralFillInputActive, + neutralFillInputHover, + neutralFillInputRest, + neutralFillRest, + neutralForegroundRest, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Text Area * @public */ export const textAreaStyles: FoundationElementTemplate = ( - context, - definition + context, + definition ) => - css` - ${display('inline-block')} :host { - font-family: ${bodyFont}; - outline: none; - user-select: none; + css` + ${display("inline-block")} :host { + font-family: ${bodyFont}; + outline: none; + user-select: none; } .control { - box-sizing: border-box; - position: relative; - color: ${neutralForegroundRest}; - background: ${neutralFillInputRest}; - border-radius: calc(${controlCornerRadius} * 1px); - border: calc(${strokeWidth} * 1px) solid ${neutralFillStrongRest}; - height: calc(${heightNumber} * 2px); - font: inherit; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - padding: calc(${designUnit} * 2px + 1px); - width: 100%; - resize: none; + box-sizing: border-box; + position: relative; + color: ${neutralForegroundRest}; + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + height: calc(${heightNumber} * 2px); + font: inherit; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + padding: calc(${designUnit} * 2px + 1px); + width: 100%; + resize: none; } .control:hover:enabled { - background: ${neutralFillInputHover}; - border-color: ${neutralFillStrongHover}; + background: ${neutralFillInputHover}; + border-color: ${accentFillHover}; } .control:active:enabled { - background: ${neutralFillInputActive}; - border-color: ${neutralFillStrongActive}; + background: ${neutralFillInputActive}; + border-color: ${accentFillActive}; } .control:hover, .control:${focusVisible}, .control:disabled, .control:active { - outline: none; + outline: none; } :host(:focus-within) .control { - border-color: ${accentFillFocus}; - box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) - ${accentFillFocus}; + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 1px ${focusStrokeOuter} inset; } - :host([appearance='filled']) .control { - background: ${neutralFillRest}; + :host([appearance="filled"]) .control { + background: ${neutralFillRest}; } - :host([appearance='filled']:hover:not([disabled])) .control { - background: ${neutralFillHover}; + :host([appearance="filled"]:hover:not([disabled])) .control { + background: ${neutralFillHover}; } - :host([resize='both']) .control { - resize: both; + :host([resize="both"]) .control { + resize: both; } - :host([resize='horizontal']) .control { - resize: horizontal; + :host([resize="horizontal"]) .control { + resize: horizontal; } - :host([resize='vertical']) .control { - resize: vertical; + :host([resize="vertical"]) .control { + resize: vertical; } .label { - display: block; - color: ${neutralForegroundRest}; - cursor: pointer; - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - margin-bottom: 4px; + display: block; + color: ${neutralForegroundRest}; + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + margin-bottom: 4px; } .label__hidden { - display: none; - visibility: hidden; + display: none; + visibility: hidden; } :host([disabled]) .label, :host([readonly]) .label, :host([readonly]) .control, :host([disabled]) .control { - cursor: ${disabledCursor}; + cursor: ${disabledCursor}; } :host([disabled]) { - opacity: ${disabledOpacity}; + opacity: ${disabledOpacity}; } :host([disabled]) .control { - border-color: ${neutralStrokeRest}; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host([disabled]) { - opacity: 1; - } - `) - ); + border-color: ${neutralStrokeRest}; + } + + :host([cols]){ + width: initial; + } + + :host([rows]) .control { + height: initial; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([disabled]) { + opacity: 1; + } + ` + ) + ); diff --git a/packages/components/src/text-field/README.md b/packages/components/src/text-field/README.md new file mode 100644 index 00000000..5ffcb797 --- /dev/null +++ b/packages/components/src/text-field/README.md @@ -0,0 +1,4 @@ +# fast-text-field +An implementation of a [text field](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/text) as a form-connected web-component. + +For more information view the [component specification](../../../fast-foundation/src/text-field/text-field.spec.md). \ No newline at end of file diff --git a/packages/components/src/text-field/fixtures/text-field.html b/packages/components/src/text-field/fixtures/text-field.html new file mode 100644 index 00000000..a12bcb34 --- /dev/null +++ b/packages/components/src/text-field/fixtures/text-field.html @@ -0,0 +1,118 @@ +

Text field

+

Default

+ +Label +Span Label + +

Full Width

+ + +

Placeholder

+ + + +

Required

+ + + +

Disabled

+ +label + + + +

Read only

+ +label + + +

Autofocus

+autofocus + + +

Maxlength

+maxlength + + +

Minlength

+minlength + + +

With start

+ + + + + + + +

With end

+ + + + + + +

Filled

+

Default

+ +label + +

Placeholder

+ + + +

Required

+ + + +

Disabled

+ +label + + + +

Read only

+ +label + + +

Visual vs audio label

+ + Visible label + + + +

With aria-label

+ + +
+ +

In a form

+ + +
+ +

One default, one with start slot, and one with end slot

+ + + + + + + + + + + diff --git a/packages/components/src/text-field/index.ts b/packages/components/src/text-field/index.ts index 2856f0d7..1113fcaa 100644 --- a/packages/components/src/text-field/index.ts +++ b/packages/components/src/text-field/index.ts @@ -1,40 +1,50 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - +import { attr } from "@microsoft/fast-element"; import { - TextField as FoundationTextField, - textFieldTemplate as template -} from '@microsoft/fast-foundation'; -import { TextField } from '@microsoft/fast-components'; -import { textFieldStyles as styles } from './text-field.styles'; + TextField as FoundationTextField, + textFieldTemplate as template, +} from "@microsoft/fast-foundation"; +import { textFieldStyles as styles } from "./text-field.styles.js"; -// TODO -// we need to add error/invalid +/** + * Text field appearances + * @public + */ +export type TextFieldAppearance = "filled" | "outline"; /** - * A function that returns a TextField registration for configuring the component with a DesignSystem. + * @internal + */ +export class TextField extends FoundationTextField { + /** + * The appearance of the element. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: TextFieldAppearance = "outline"; +} + +/** + * A function that returns a {@link @microsoft/fast-foundation#TextField} registration for configuring the component with a DesignSystem. + * Implements {@link @microsoft/fast-foundation#textFieldTemplate} * * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * * {@link https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/delegatesFocus | delegatesFocus} */ -export const jpTextField = TextField.compose({ - baseName: 'text-field', - baseClass: FoundationTextField, - template, - styles, - shadowOptions: { - delegatesFocus: true - } +export const fastTextField = TextField.compose({ + baseName: "text-field", + baseClass: FoundationTextField, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, }); -export { TextField, TextFieldAppearance } from '@microsoft/fast-components'; - -/** - * Styles for TextField - * @public - */ export { styles as textFieldStyles }; diff --git a/packages/components/src/text-field/scenarios/index.html b/packages/components/src/text-field/scenarios/index.html new file mode 100644 index 00000000..adc629f4 --- /dev/null +++ b/packages/components/src/text-field/scenarios/index.html @@ -0,0 +1,3 @@ + diff --git a/packages/components/src/text-field/text-field.open-ui.definition.ts b/packages/components/src/text-field/text-field.open-ui.definition.ts new file mode 100644 index 00000000..c8e7c609 --- /dev/null +++ b/packages/components/src/text-field/text-field.open-ui.definition.ts @@ -0,0 +1,4 @@ +export default { + name: "Text field", + url: "https://fast.design/docs/components/text-field", +}; diff --git a/packages/components/src/text-field/text-field.stories.ts b/packages/components/src/text-field/text-field.stories.ts index e850ca96..7f05a111 100644 --- a/packages/components/src/text-field/text-field.stories.ts +++ b/packages/components/src/text-field/text-field.stories.ts @@ -1,146 +1,24 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { getFaIcon, setTheme } from '../utilities/storybook'; -import { TextField } from './index'; - -export default { - title: 'Components/Text Field', - argTypes: { - label: { control: 'text' }, - placeholder: { control: 'text' }, - value: { control: 'text' }, - maxLength: { control: 'number' }, - size: { control: 'number' }, - type: { - control: 'select', - options: ['Email', 'Password', 'Tel', 'Text', 'Url'] - }, - isReadOnly: { control: 'boolean' }, - isDisabled: { control: 'boolean' }, - isAutoFocused: { control: 'boolean' }, - startIcon: { control: 'boolean' }, - endIcon: { control: 'boolean' }, - appearance: { control: 'radio', options: ['outline', 'filled'] }, - onChange: { - action: 'changed', - table: { - disable: true - } +import { STORY_RENDERED } from "@storybook/core-events"; +import addons from "@storybook/addons"; +import TextFieldTemplate from "./fixtures/text-field.html"; +import "./index.js"; + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("text-field")) { + document.querySelectorAll(".form").forEach((el: HTMLFormElement) => { + el.onsubmit = event => { + console.log(event, "event"); + event.preventDefault(); + const form: HTMLFormElement = document.forms["myForm"]; + + console.log(form.elements["fname"].value, "value of input"); + }; + }); } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - ${args.startIcon ? getFaIcon('search', 'start') : ''} - ${args.label} - ${args.endIcon ? getFaIcon('euro-sign', 'end') : ''} - ` - ); - - const textField = container.firstChild as TextField; - - if (args.value) { - textField.value = args.value; - } - - if (args.onChange) { - textField.addEventListener('change', args.onChange); - } +}); - return textField; -}; - -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - label: 'Text Field Label', - placeholder: '', - value: '', - maxLength: '', - size: '', - type: 'Text', - isReadOnly: false, - isDisabled: false, - isAutoFocused: false, - startIcon: false, - endIcon: false, - appearance: 'outline', - onChange: action('text-field-onchange') -}; - -export const WithPlaceholder: StoryObj = { render: Template.bind({}) }; -WithPlaceholder.args = { - ...Default.args, - placeholder: 'Placeholder Text' -}; - -export const WithAutofocus: StoryObj = { render: Template.bind({}) }; -WithAutofocus.args = { - ...Default.args, - autofocus: true -}; - -export const WithDisabled: StoryObj = { render: Template.bind({}) }; -WithDisabled.args = { - ...Default.args, - disabled: true -}; - -export const WithSize: StoryObj = { render: Template.bind({}) }; -WithSize.args = { - ...Default.args, - placeholder: 'This text field is 50 characters in width', - size: 50 -}; - -export const WithType: StoryObj = { render: Template.bind({}) }; -WithType.args = { - ...Default.args, - placeholder: 'This text field has type password', - type: 'Password' -}; - -export const WithMaxLength: StoryObj = { render: Template.bind({}) }; -WithMaxLength.args = { - ...Default.args, - placeholder: 'This text field can only contain a maximum of 10 characters', - maxLength: 10 -}; - -export const WithReadonly: StoryObj = { render: Template.bind({}) }; -WithReadonly.args = { - ...Default.args, - readonly: true -}; - -export const WithStartIcon: StoryObj = { render: Template.bind({}) }; -WithStartIcon.args = { - ...Default.args, - startIcon: true +export default { + title: "Text Field", }; -export const WithEndIcon: StoryObj = { render: Template.bind({}) }; -WithEndIcon.args = { - ...Default.args, - endIcon: true -}; +export const TextField = () => TextFieldTemplate; diff --git a/packages/components/src/text-field/text-field.styles.ts b/packages/components/src/text-field/text-field.styles.ts index 255e35f1..7881b1cc 100644 --- a/packages/components/src/text-field/text-field.styles.ts +++ b/packages/components/src/text-field/text-field.styles.ts @@ -1,26 +1,198 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + TextFieldOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - FoundationElementTemplate, - TextFieldOptions -} from '@microsoft/fast-foundation'; -import { BaseFieldStyles } from '../styles/index'; + accentFillActive, + accentFillHover, + accentFillRest, + bodyFont, + controlCornerRadius, + designUnit, + disabledOpacity, + focusStrokeOuter, + focusStrokeWidth, + neutralFillHover, + neutralFillInputHover, + neutralFillInputRest, + neutralFillRest, + neutralForegroundRest, + neutralStrokeRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { heightNumber } from "../styles/index.js"; /** * Styles for Text Field * @public */ export const textFieldStyles: FoundationElementTemplate< - ElementStyles, - TextFieldOptions -> = (context, definition) => css` - ${BaseFieldStyles} + ElementStyles, + TextFieldOptions +> = (context, definition) => + css` + ${display("inline-block")} :host { + font-family: ${bodyFont}; + outline: none; + user-select: none; + } + + .root { + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: row; + color: ${neutralForegroundRest}; + background: ${neutralFillInputRest}; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${accentFillRest}; + height: calc(${heightNumber} * 1px); + align-items: baseline; + } + + .control { + -webkit-appearance: none; + font: inherit; + background: transparent; + border: 0; + color: inherit; + height: calc(100% - 4px); + width: 100%; + margin-top: auto; + margin-bottom: auto; + border: none; + padding: 0 calc(${designUnit} * 2px + 1px); + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + } + + .control:hover, + .control:${focusVisible}, + .control:disabled, + .control:active { + outline: none; + } + + .label { + display: block; + color: ${neutralForegroundRest}; + cursor: pointer; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + margin-bottom: 4px; + } - .start, + .label__hidden { + display: none; + visibility: hidden; + } + + .start, + .control, + .end { + align-self: center; + } + + .start, .end { - display: flex; - } -`; + display: flex; + margin: auto; + fill: currentcolor; + } + + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + } + + .start { + margin-inline-start: 11px; + } + + .end { + margin-inline-end: 11px; + } + + :host(:hover:not([disabled])) .root { + background: ${neutralFillInputHover}; + border-color: ${accentFillHover}; + } + + :host(:active:not([disabled])) .root { + background: ${neutralFillInputHover}; + border-color: ${accentFillActive}; + } + + :host(:focus-within:not([disabled])) .root { + border-color: ${focusStrokeOuter}; + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${focusStrokeOuter} inset; + } + + :host([appearance="filled"]) .root { + background: ${neutralFillRest}; + } + + :host([appearance="filled"]:hover:not([disabled])) .root { + background: ${neutralFillHover}; + } + + :host([disabled]) .label, + :host([readonly]) .label, + :host([readonly]) .control, + :host([disabled]) .control { + cursor: ${disabledCursor}; + } + + :host([disabled]) { + opacity: ${disabledOpacity}; + } + + :host([disabled]) .control { + border-color: ${neutralStrokeRest}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior( + css` + .root, + :host([appearance="filled"]) .root { + forced-color-adjust: none; + background: ${SystemColors.Field}; + border-color: ${SystemColors.FieldText}; + } + :host(:hover:not([disabled])) .root, + :host([appearance="filled"]:hover:not([disabled])) .root, + :host([appearance="filled"]:hover) .root { + background: ${SystemColors.Field}; + border-color: ${SystemColors.Highlight}; + } + .start, + .end { + fill: currentcolor; + } + :host([disabled]) { + opacity: 1; + } + :host([disabled]) .root, + :host([appearance="filled"]:hover[disabled]) .root { + border-color: ${SystemColors.GrayText}; + background: ${SystemColors.Field}; + } + :host(:focus-within:enabled) .root { + border-color: ${SystemColors.Highlight}; + box-shadow: 0 0 0 1px ${SystemColors.Highlight} inset; + } + input::placeholder { + color: ${SystemColors.GrayText}; + } + ` + ) + ); diff --git a/packages/components/src/text-field/text-field.vscode.definition.ts b/packages/components/src/text-field/text-field.vscode.definition.ts new file mode 100644 index 00000000..02a4fda2 --- /dev/null +++ b/packages/components/src/text-field/text-field.vscode.definition.ts @@ -0,0 +1,160 @@ +export default { + version: 1.1, + tags: [ + { + name: "fast-text-field", + title: "Text field", + description: "The FAST text-field element", + attributes: [ + { + name: "value", + title: "Value", + description: "The HTML value attribute of the text field", + required: false, + type: "string", + }, + { + name: "appearance", + title: "Appearance", + description: "The text field's visual treatment", + default: "outline", + values: [{ name: "outline" }, { name: "filled" }], + type: "string", + required: false, + }, + { + name: "autofocus", + title: "Autofocus", + description: + "Determines if the element should receive document focus on page load", + required: false, + type: "boolean", + default: false, + }, + { + name: "placeholder", + title: "Placeholder", + description: + "Sets the placeholder value of the element, generally used to provide a hint to the user", + required: false, + type: "string", + }, + { + name: "type", + title: "Type", + description: "Specifies the type of text input for the field", + default: "text", + values: [ + { name: "email" }, + { name: "password" }, + { name: "tel" }, + { name: "text" }, + { name: "url" }, + ], + type: "string", + required: false, + }, + { + name: "list", + title: "List ID", + description: "Allows associating a datalist to the component", + required: false, + type: "string", + default: "", + }, + { + name: "maxlength", + title: "Maximum length", + description: "The maximum number of characters a user can enter", + required: false, + type: "number", + }, + { + name: "minlength", + title: "Minimum length", + description: "The minimum number of characters a user can enter", + required: false, + type: "number", + }, + { + name: "pattern", + title: "Validation pattern", + description: + "A regular expression that the value must match to pass validation", + required: false, + type: "string", + }, + { + name: "size", + title: "Size", + description: + "Sets the width of the element to a specified number of characters", + required: false, + type: "number", + }, + { + name: "spellcheck", + title: "Spellcheck", + description: + "Controls whether or not to enable spell checking for the input field, or if the default spell checking configuration should be used", + required: false, + type: "boolean", + }, + { + name: "name", + title: "Name", + description: + "This element's value will be surfaced during form submission under the provided name", + type: "string", + default: "", + required: false, + }, + { + name: "required", + title: "Required", + description: + "Require the field to be completed prior to form submission", + type: "boolean", + default: false, + required: false, + }, + { + name: "disabled", + title: "Disabled", + description: "Sets the disabled state of the number field", + type: "boolean", + default: false, + required: false, + }, + { + name: "readonly", + title: "Readonly", + description: + "When true, the control will be immutable by user interaction", + type: "boolean", + default: false, + required: false, + }, + ], + slots: [ + { + name: "", + title: "Default slot", + description: "The content of the radio represents its visual label", + }, + { + name: "start", + title: "Start slot", + description: + "Contents of the start slot are positioned before the option content", + }, + { + name: "end", + title: "End slot", + description: + "Contents of the end slot are positioned after the option content", + }, + ], + }, + ], +}; diff --git a/packages/components/src/toolbar/index.ts b/packages/components/src/toolbar/index.ts index ff1a0226..0caae573 100644 --- a/packages/components/src/toolbar/index.ts +++ b/packages/components/src/toolbar/index.ts @@ -1,12 +1,32 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { Toolbar } from '@microsoft/fast-components'; import { - Toolbar as FoundationToolbar, - toolbarTemplate as template -} from '@microsoft/fast-foundation'; -import { toolbarStyles as styles } from './toolbar.styles'; + composedParent, + Toolbar as FoundationToolbar, + toolbarTemplate as template, +} from "@microsoft/fast-foundation"; +import { Swatch } from "../color/swatch.js"; +import { fillColor, neutralFillLayerRecipe } from "../design-tokens.js"; +import { toolbarStyles as styles } from "./toolbar.styles.js"; + +/** + * @internal + */ +export class Toolbar extends FoundationToolbar { + connectedCallback() { + super.connectedCallback(); + + const parent = composedParent(this); + + if (parent) { + fillColor.setValueFor( + this, + (target: HTMLElement): Swatch => + neutralFillLayerRecipe + .getValueFor(target) + .evaluate(target, fillColor.getValueFor(parent)) + ); + } + } +} /** * A function that returns a {@link @microsoft/fast-foundation#Toolbar} registration for configuring the component with a DesignSystem. @@ -15,17 +35,17 @@ import { toolbarStyles as styles } from './toolbar.styles'; * @public * @remarks * - * Generates HTML Element: `` + * Generates HTML Element: `` * */ -export const jpToolbar = Toolbar.compose({ - baseName: 'toolbar', - baseClass: FoundationToolbar, - template, - styles, - shadowOptions: { - delegatesFocus: true - } +export const fastToolbar = Toolbar.compose({ + baseName: "toolbar", + baseClass: FoundationToolbar, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, }); -export { Toolbar, styles as toolbarStyles }; +export { styles as toolbarStyles }; diff --git a/packages/components/src/toolbar/toolbar.stories.ts b/packages/components/src/toolbar/toolbar.stories.ts index 62da981b..e26fef8b 100644 --- a/packages/components/src/toolbar/toolbar.stories.ts +++ b/packages/components/src/toolbar/toolbar.stories.ts @@ -1,58 +1,7 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +import ToolbarTemplate from "./fixtures/base.html"; export default { - title: 'Components/Toolbar', - argTypes: { - startSlot: { control: 'boolean' }, - endSlot: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - }, - decorators: [ - story => ` - ${story()}` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - Button - ${args.endSlot ? 'End Slot Button' : ''} - ${ - args.startSlot - ? 'Start Slot Button' - : '' - } - - Option 1 - Second option - Option 3 - - Checkbox 1 - Checkbox 2 - `; + title: "Toolbar", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - startSlot: false, - endSlot: false -}; +export const Toolbar = () => ToolbarTemplate; diff --git a/packages/components/src/toolbar/toolbar.styles.ts b/packages/components/src/toolbar/toolbar.styles.ts index c59017c8..f624669f 100644 --- a/packages/components/src/toolbar/toolbar.styles.ts +++ b/packages/components/src/toolbar/toolbar.styles.ts @@ -1,89 +1,84 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, ElementStyles } from '@microsoft/fast-element'; +import { css, ElementStyles } from "@microsoft/fast-element"; import { - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - ToolbarOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + ToolbarOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; import { - accentFillFocus, - controlCornerRadius, - fillColor, - focusStrokeWidth, - strokeWidth -} from '../design-tokens'; + controlCornerRadius, + fillColor, + focusStrokeWidth, + neutralStrokeFocus, + strokeWidth, +} from "../design-tokens.js"; /** * Styles for the Toolbar * * @public */ -export const toolbarStyles: FoundationElementTemplate< - ElementStyles, - ToolbarOptions -> = (context, definition) => - css` - ${display('inline-flex')} :host { - --toolbar-item-gap: calc( - (var(--design-unit) + calc(var(--density) + 2)) * 1px - ); - background-color: ${fillColor}; - border-radius: calc(${controlCornerRadius} * 1px); - fill: currentcolor; - padding: var(--toolbar-item-gap); - } +export const toolbarStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + ${display("inline-flex")} :host { + --toolbar-item-gap: calc( + (var(--design-unit) + calc(var(--density) + 2)) * 1px + ); + background-color: ${fillColor}; + border-radius: calc(${controlCornerRadius} * 1px); + fill: currentcolor; + padding: var(--toolbar-item-gap); + } - :host(${focusVisible}) { - outline: calc(${strokeWidth} * 1px) solid ${accentFillFocus}; - } + :host(${focusVisible}) { + outline: calc(${strokeWidth} * 1px) solid ${neutralStrokeFocus}; + } - .positioning-region { - align-items: flex-start; - display: inline-flex; - flex-flow: row wrap; - justify-content: flex-start; - width: 100%; - height: 100%; - } + .positioning-region { + align-items: flex-start; + display: inline-flex; + flex-flow: row wrap; + justify-content: flex-start; + } - :host([orientation='vertical']) .positioning-region { - flex-direction: column; - } + :host([orientation="vertical"]) .positioning-region { + flex-direction: column; + } - ::slotted(:not([slot])) { - flex: 0 0 auto; - margin: 0 var(--toolbar-item-gap); - } + ::slotted(:not([slot])) { + flex: 0 0 auto; + margin: 0 var(--toolbar-item-gap); + } - :host([orientation='vertical']) ::slotted(:not([slot])) { - margin: var(--toolbar-item-gap) 0; - } + :host([orientation="vertical"]) ::slotted(:not([slot])) { + margin: var(--toolbar-item-gap) 0; + } - .start, - .end { - display: flex; - margin: auto; - margin-inline: 0; - } + .start, + .end { + display: flex; + margin: auto; + margin-inline: 0; + } - ::slotted(svg) { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - width: 16px; - height: 16px; - } - `.withBehaviors( - forcedColorsStylesheetBehavior(css` - :host(:${focusVisible}) { - box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) - ${SystemColors.Highlight}; - color: ${SystemColors.ButtonText}; - forced-color-adjust: none; - } - `) - ); + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + } + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host(:${focusVisible}) { + box-shadow: 0 0 0 calc(${focusStrokeWidth} * 1px) ${SystemColors.Highlight}; + color: ${SystemColors.ButtonText}; + forced-color-adjust: none; + } + ` + ) + ); diff --git a/packages/components/src/tooltip/index.ts b/packages/components/src/tooltip/index.ts index f64264c3..44b29276 100644 --- a/packages/components/src/tooltip/index.ts +++ b/packages/components/src/tooltip/index.ts @@ -1,11 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - tooltipTemplate as template, - Tooltip -} from '@microsoft/fast-foundation'; -import { tooltipStyles as styles } from '@microsoft/fast-components'; +import { tooltipTemplate as template, Tooltip } from "@microsoft/fast-foundation"; +import { tooltipStyles as styles } from "./tooltip.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#Tooltip} registration for configuring the component with a DesignSystem. @@ -14,12 +8,12 @@ import { tooltipStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` */ -export const jpTooltip = Tooltip.compose({ - baseName: 'tooltip', - template, - styles +export const fastTooltip = Tooltip.compose({ + baseName: "tooltip", + template, + styles, }); /** diff --git a/packages/components/src/tooltip/tooltip.stories.ts b/packages/components/src/tooltip/tooltip.stories.ts index e24d28fa..7aa911f7 100644 --- a/packages/components/src/tooltip/tooltip.stories.ts +++ b/packages/components/src/tooltip/tooltip.stories.ts @@ -1,60 +1,74 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. +import { STORY_RENDERED } from "@storybook/core-events"; +import addons from "@storybook/addons"; +import type { Tooltip as FoundationTooltip } from "@microsoft/fast-foundation"; +import TooltipTemplate from "./fixtures/base.html"; -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { setTheme } from '../utilities/storybook'; +function onShowClick(): void { + for (let i = 1; i <= 4; i++) { + const tooltipInstance = document.getElementById( + `tooltip-show-${i}` + ) as FoundationTooltip; + tooltipInstance.visible = !tooltipInstance.visible; + } +} +function onShowCornersClick(): void { + for (let i = 1; i <= 4; i++) { + const tooltipInstance = document.getElementById( + `tooltip-show-corners-${i}` + ) as FoundationTooltip; + tooltipInstance.visible = !tooltipInstance.visible; + } +} -export default { - title: 'Components/Tooltip', - argTypes: { - visible: { control: 'boolean' }, - position: { - control: 'select', - options: ['top', 'right', 'bottom', 'left', 'start', 'end'] - }, - delay: { - description: 'Delay to display the tooltip (in ms)', - table: { - defaultValue: { summary: 300 } - }, - control: { type: 'range', min: 0, max: 2000, step: 100 } +//check changing anchor by changing anchorElement +function onAnchorMouseEnterProp(e: MouseEvent): void { + if (!e.target) { + return; } - }, - parameters: { - controls: { expanded: true }, - actions: { - disabled: true + + const tooltipInstance = document.getElementById( + "tooltip-anchor-switch" + ) as FoundationTooltip; + tooltipInstance.anchorElement = e.target as HTMLElement; +} + +//check changing anchor by setting attribute +function onAnchorMouseEnterAttribute(e: MouseEvent): void { + if (!e.target) { + return; } - }, - decorators: [ - story => `
- ${story()} -
` - ] -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - Helpful text is helpful - - - anchor - `; -}; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - visible: false, - position: 'top', - delay: 300 + const tooltipInstance = document.getElementById( + "tooltip-anchor-switch" + ) as FoundationTooltip; + tooltipInstance.setAttribute("anchor", (e.target as HTMLElement).id); +} + +addons.getChannel().addListener(STORY_RENDERED, (name: string) => { + if (name.toLowerCase().startsWith("tooltip")) { + document + .querySelectorAll("fast-button[id^=anchor-anchor-switch-prop]") + .forEach((el: HTMLElement) => { + el.addEventListener("mouseenter", onAnchorMouseEnterProp); + }); + + document + .querySelectorAll("fast-button[id^=anchor-anchor-switch-attribute]") + .forEach((el: HTMLElement) => { + el.addEventListener("mouseenter", onAnchorMouseEnterAttribute); + }); + + const showButton = document.getElementById("anchor-show") as HTMLElement; + showButton.addEventListener("click", onShowClick); + const showButtonCorners = document.getElementById( + "anchor-show-corners" + ) as HTMLElement; + showButtonCorners.addEventListener("click", onShowCornersClick); + } +}); + +export default { + title: "Tooltip", }; + +export const Tooltip = () => TooltipTemplate; diff --git a/packages/components/src/tooltip/tooltip.styles.ts b/packages/components/src/tooltip/tooltip.styles.ts new file mode 100644 index 00000000..5bd0ccd6 --- /dev/null +++ b/packages/components/src/tooltip/tooltip.styles.ts @@ -0,0 +1,116 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { + AnchoredRegion, + ElementDefinitionContext, + forcedColorsStylesheetBehavior, + FoundationElementDefinition, +} from "@microsoft/fast-foundation"; +import { + bodyFont, + controlCornerRadius, + focusStrokeOuter, + neutralFillRest, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; + +/** + * Styles for Tooltip + * @public + */ +export const tooltipStyles: ( + context: ElementDefinitionContext, + definition: FoundationElementDefinition +) => ElementStyles = ( + context: ElementDefinitionContext, + definition: FoundationElementDefinition +) => { + const anchoredRegionTag = context.tagFor(AnchoredRegion); + return css` + :host { + contain: size; + overflow: visible; + height: 0; + width: 0; + } + + .tooltip { + box-sizing: border-box; + border-radius: calc(${controlCornerRadius} * 1px); + border: calc(${strokeWidth} * 1px) solid ${focusStrokeOuter}; + box-shadow: 0 0 0 1px ${focusStrokeOuter} inset; + background: ${neutralFillRest}; + color: ${neutralForegroundRest}; + padding: 4px; + height: fit-content; + width: fit-content; + font-family: ${bodyFont}; + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + white-space: nowrap; + /* TODO: a mechanism to manage z-index across components + https://github.com/microsoft/fast/issues/3813 */ + z-index: 10000; + } + + ${anchoredRegionTag} { + display: flex; + justify-content: center; + align-items: center; + overflow: visible; + flex-direction: row; + } + + ${anchoredRegionTag}.right, + ${anchoredRegionTag}.left { + flex-direction: column; + } + + ${anchoredRegionTag}.top .tooltip { + margin-bottom: 4px; + } + + ${anchoredRegionTag}.bottom .tooltip { + margin-top: 4px; + } + + ${anchoredRegionTag}.left .tooltip { + margin-right: 4px; + } + + ${anchoredRegionTag}.right .tooltip { + margin-left: 4px; + } + + ${anchoredRegionTag}.top.left .tooltip, + ${anchoredRegionTag}.top.right .tooltip { + margin-bottom: 0px; + } + + ${anchoredRegionTag}.bottom.left .tooltip, + ${anchoredRegionTag}.bottom.right .tooltip { + margin-top: 0px; + } + + ${anchoredRegionTag}.top.left .tooltip, + ${anchoredRegionTag}.bottom.left .tooltip { + margin-right: 0px; + } + + ${anchoredRegionTag}.top.right .tooltip, + ${anchoredRegionTag}.bottom.right .tooltip { + margin-left: 0px; + } + + `.withBehaviors( + forcedColorsStylesheetBehavior( + css` + :host([disabled]) { + opacity: 1; + } + ` + ) + ); +}; diff --git a/packages/components/src/tree-item/index.ts b/packages/components/src/tree-item/index.ts index dddecf74..2e771a07 100644 --- a/packages/components/src/tree-item/index.ts +++ b/packages/components/src/tree-item/index.ts @@ -1,12 +1,9 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - import { - treeItemTemplate as template, - TreeItem, - TreeItemOptions -} from '@microsoft/fast-foundation'; -import { treeItemStyles as styles } from './tree-item.styles'; + treeItemTemplate as template, + TreeItem, + TreeItemOptions, +} from "@microsoft/fast-foundation"; +import { treeItemStyles as styles } from "./tree-item.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#TreeItem} registration for configuring the component with a DesignSystem. @@ -15,14 +12,14 @@ import { treeItemStyles as styles } from './tree-item.styles'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * */ -export const jpTreeItem = TreeItem.compose({ - baseName: 'tree-item', - template, - styles, - expandCollapseGlyph: /* html */ ` +export const fastTreeItem = TreeItem.compose({ + baseName: "tree-item", + template, + styles, + expandCollapseGlyph: /* html */ ` ({ d="M5.00001 12.3263C5.00124 12.5147 5.05566 12.699 5.15699 12.8578C5.25831 13.0167 5.40243 13.1437 5.57273 13.2242C5.74304 13.3047 5.9326 13.3354 6.11959 13.3128C6.30659 13.2902 6.4834 13.2152 6.62967 13.0965L10.8988 8.83532C11.0739 8.69473 11.2153 8.51658 11.3124 8.31402C11.4096 8.11146 11.46 7.88966 11.46 7.66499C11.46 7.44033 11.4096 7.21853 11.3124 7.01597C11.2153 6.81341 11.0739 6.63526 10.8988 6.49467L6.62967 2.22347C6.48274 2.10422 6.30501 2.02912 6.11712 2.00691C5.92923 1.9847 5.73889 2.01628 5.56823 2.09799C5.39757 2.17969 5.25358 2.30817 5.153 2.46849C5.05241 2.62882 4.99936 2.8144 5.00001 3.00369V12.3263Z" /> - ` + `, }); /** diff --git a/packages/components/src/tree-item/tree-item.stories.ts b/packages/components/src/tree-item/tree-item.stories.ts index e5e95c7c..c28557ff 100644 --- a/packages/components/src/tree-item/tree-item.stories.ts +++ b/packages/components/src/tree-item/tree-item.stories.ts @@ -1,59 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { getFaIcon, setTheme } from '../utilities/storybook'; +import TreeItemTemplate from "./fixtures/tree-item.html"; +import "./index.js"; export default { - title: 'Components/Tree Item', - argTypes: { - selected: { control: 'boolean' }, - beforeContent: { control: 'boolean' }, - afterContent: { control: 'boolean' } - }, - parameters: { - actions: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): string => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - - return ` - ${args.beforeContent ? getFaIcon('robot', 'start') : ''} - Tree item - ${args.afterContent ? getFaIcon('robot', 'end') : ''} - TreeItemTemplate; diff --git a/packages/components/src/tree-item/tree-item.styles.ts b/packages/components/src/tree-item/tree-item.styles.ts index 5066ea39..acd5dc75 100644 --- a/packages/components/src/tree-item/tree-item.styles.ts +++ b/packages/components/src/tree-item/tree-item.styles.ts @@ -1,78 +1,69 @@ -// Copyright (c) Jupyter Development Team. -// Copyright (c) Microsoft Corporation. -// Distributed under the terms of the Modified BSD License. - -import { css, cssPartial, ElementStyles } from '@microsoft/fast-element'; +import { css, cssPartial, ElementStyles } from "@microsoft/fast-element"; import { - DesignToken, - disabledCursor, - display, - focusVisible, - forcedColorsStylesheetBehavior, - FoundationElementTemplate, - TreeItem, - TreeItemOptions -} from '@microsoft/fast-foundation'; -import { SystemColors } from '@microsoft/fast-web-utilities'; -import type { Swatch } from '../color'; + DesignToken, + disabledCursor, + display, + focusVisible, + forcedColorsStylesheetBehavior, + FoundationElementTemplate, + TreeItem, + TreeItemOptions, +} from "@microsoft/fast-foundation"; +import { SystemColors } from "@microsoft/fast-web-utilities"; +import { Swatch } from "../color/swatch.js"; import { - accentFillFocus, - accentForegroundRest, - baseHeightMultiplier, - bodyFont, - controlCornerRadius, - density, - designUnit, - DirectionalStyleSheetBehavior, - disabledOpacity, - focusStrokeWidth, - neutralFillRecipe, - neutralFillRest, - neutralFillStealthActive, - neutralFillStealthHover, - neutralFillStealthRecipe, - neutralFillStealthRest, - neutralForegroundRest, - strokeWidth, - typeRampBaseFontSize, - typeRampBaseLineHeight -} from '../design-tokens'; -import { heightNumber } from '../styles/index'; + accentForegroundRest, + baseHeightMultiplier, + bodyFont, + controlCornerRadius, + density, + designUnit, + disabledOpacity, + focusStrokeOuter, + focusStrokeWidth, + neutralFillActive, + neutralFillHover, + neutralFillRecipe, + neutralFillRest, + neutralFillStealthActive, + neutralFillStealthHover, + neutralFillStealthRecipe, + neutralFillStealthRest, + neutralForegroundRest, + strokeWidth, + typeRampBaseFontSize, + typeRampBaseLineHeight, +} from "../design-tokens.js"; +import { DirectionalStyleSheetBehavior, heightNumber } from "../styles/index.js"; const ltr = css` - .expand-collapse-glyph { - transform: rotate(0deg); - } - :host(.nested) .expand-collapse-button { - left: var( - --expand-collapse-button-nested-width, - calc(${heightNumber} * -1px) - ); - } - :host([selected])::after { - left: calc(${focusStrokeWidth} * 1px); - } - :host([expanded]) > .positioning-region .expand-collapse-glyph { - transform: rotate(90deg); - } + .expand-collapse-glyph { + transform: rotate(0deg); + } + :host(.nested) .expand-collapse-button { + left: var(--expand-collapse-button-nested-width, calc(${heightNumber} * -1px)); + } + :host([selected])::after { + left: calc(${focusStrokeWidth} * 1px); + } + :host([expanded]) > .positioning-region .expand-collapse-glyph { + transform: rotate(45deg); + } `; const rtl = css` - .expand-collapse-glyph { - transform: rotate(180deg); - } - :host(.nested) .expand-collapse-button { - right: var( - --expand-collapse-button-nested-width, - calc(${heightNumber} * -1px) - ); - } - :host([selected])::after { - right: calc(${focusStrokeWidth} * 1px); - } - :host([expanded]) > .positioning-region .expand-collapse-glyph { - transform: rotate(90deg); - } + .expand-collapse-glyph { + transform: rotate(180deg); + } + :host(.nested) .expand-collapse-button { + right: var(--expand-collapse-button-nested-width, calc(${heightNumber} * -1px)); + } + :host([selected])::after { + right: calc(${focusStrokeWidth} * 1px); + } + :host([expanded]) > .positioning-region .expand-collapse-glyph { + transform: rotate(135deg); + } `; /** @@ -82,260 +73,280 @@ const rtl = css` export const expandCollapseButtonSize = cssPartial`((${baseHeightMultiplier} / 2) * ${designUnit}) + ((${designUnit} * ${density}) / 2)`; const expandCollapseHoverBehavior = DesignToken.create( - 'tree-item-expand-collapse-hover' + "tree-item-expand-collapse-hover" ).withDefault((target: HTMLElement) => { - const recipe = neutralFillStealthRecipe.getValueFor(target); - return recipe.evaluate(target, recipe.evaluate(target).hover).hover; + const recipe = neutralFillStealthRecipe.getValueFor(target); + return recipe.evaluate(target, recipe.evaluate(target).hover).hover; }); const selectedExpandCollapseHoverBehavior = DesignToken.create( - 'tree-item-expand-collapse-selected-hover' + "tree-item-expand-collapse-selected-hover" ).withDefault((target: HTMLElement) => { - const baseRecipe = neutralFillRecipe.getValueFor(target); - const buttonRecipe = neutralFillStealthRecipe.getValueFor(target); - return buttonRecipe.evaluate(target, baseRecipe.evaluate(target).rest).hover; + const baseRecipe = neutralFillRecipe.getValueFor(target); + const buttonRecipe = neutralFillStealthRecipe.getValueFor(target); + return buttonRecipe.evaluate(target, baseRecipe.evaluate(target).rest).hover; }); /** * Styles for Tree Item * @public */ -export const treeItemStyles: FoundationElementTemplate< - ElementStyles, - TreeItemOptions -> = (context, definition) => - css` - ${display('block')} :host { - contain: content; - position: relative; - outline: none; - color: ${neutralForegroundRest}; - background: ${neutralFillStealthRest}; - cursor: pointer; - font-family: ${bodyFont}; - --expand-collapse-button-size: calc(${heightNumber} * 1px); - --tree-item-nested-width: 0; +export const treeItemStyles: FoundationElementTemplate = ( + context, + definition +) => + css` + /** + * This animation exists because when tree item children are conditionally loaded + * there is a visual bug where the DOM exists but styles have not yet been applied (essentially FOUC). + * This subtle animation provides a ever so slight timing adjustment for loading that solves the issue. + */ + @keyframes treeItemLoading { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } } - :host(:focus) > .positioning-region { - outline: none; + ${display("block")} :host { + contain: content; + position: relative; + outline: none; + color: ${neutralForegroundRest}; + background: ${neutralFillStealthRest}; + cursor: pointer; + font-family: ${bodyFont}; + --expand-collapse-button-size: calc(${heightNumber} * 1px); + --tree-item-nested-width: 0; } - :host(:focus) .content-region { - outline: none; - } - - :host(:${focusVisible}) .positioning-region { - border-color: ${accentFillFocus}; - box-shadow: 0 0 0 calc((${focusStrokeWidth} - ${strokeWidth}) * 1px) - ${accentFillFocus} inset; - color: ${neutralForegroundRest}; - } - - .positioning-region { - display: flex; - position: relative; - box-sizing: border-box; - border: transparent calc(${strokeWidth} * 1px) solid; - border-radius: calc(${controlCornerRadius} * 1px); - height: calc((${heightNumber} + 1) * 1px); - } - - .positioning-region::before { - content: ''; - display: block; - width: var(--tree-item-nested-width); - flex-shrink: 0; - } - - .positioning-region:hover { - background: ${neutralFillStealthHover}; - } - - .positioning-region:active { - background: ${neutralFillStealthActive}; - } - - .content-region { - display: inline-flex; - align-items: center; - white-space: nowrap; - width: 100%; - min-width: 0; - height: calc(${heightNumber} * 1px); - margin-inline-start: calc(${designUnit} * 2px + 8px); - font-size: ${typeRampBaseFontSize}; - line-height: ${typeRampBaseLineHeight}; - font-weight: 400; - } - - .items { - display: none; - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - font-size: calc(1em + (${designUnit} + 16) * 1px); - } - - .expand-collapse-button { - background: none; - border: none; - outline: none; - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - width: calc((${expandCollapseButtonSize} + (${designUnit} * 2)) * 1px); - height: calc((${expandCollapseButtonSize} + (${designUnit} * 2)) * 1px); - padding: 0; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - margin-left: 6px; - margin-right: 6px; - } - - .expand-collapse-glyph { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - width: 16px; - height: 16px; - transition: transform 0.1s linear; - - pointer-events: none; - fill: currentcolor; - } - - .start, - .end { - display: flex; - fill: currentcolor; - } - - ::slotted(svg) { - /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ - width: 16px; - height: 16px; - } - - .start { - /* TODO: horizontalSpacing https://github.com/microsoft/fast/issues/2766 */ - margin-inline-end: calc(${designUnit} * 2px + 2px); - } - - .end { - /* TODO: horizontalSpacing https://github.com/microsoft/fast/issues/2766 */ - margin-inline-start: calc(${designUnit} * 2px + 2px); - } - - :host([expanded]) > .items { - display: block; - } - - :host([disabled]) .content-region { - opacity: ${disabledOpacity}; - cursor: ${disabledCursor}; - } - - :host(.nested) .content-region { - position: relative; - margin-inline-start: var(--expand-collapse-button-size); - } - - :host(.nested) .expand-collapse-button { - position: absolute; - } - - :host(.nested) .expand-collapse-button:hover { - background: ${expandCollapseHoverBehavior}; - } - - :host([selected]) .positioning-region { - background: ${neutralFillRest}; - } - - :host([selected]) .expand-collapse-button:hover { - background: ${selectedExpandCollapseHoverBehavior}; - } - - :host([selected])::after { - /* The background needs to be calculated based on the selected background state - for this control. We currently have no way of changing that, so setting to - accent-foreground-rest for the time being */ - background: ${accentForegroundRest}; - border-radius: calc(${controlCornerRadius} * 1px); - content: ''; - display: block; - position: absolute; - top: calc((${heightNumber} / 4) * 1px); - width: 3px; - height: calc((${heightNumber} / 2) * 1px); - } - - ::slotted(${context.tagFor(TreeItem)}) { - --tree-item-nested-width: 1em; - --expand-collapse-button-nested-width: calc(${heightNumber} * -1px); - } - `.withBehaviors( - new DirectionalStyleSheetBehavior(ltr, rtl), - forcedColorsStylesheetBehavior(css` - :host { - forced-color-adjust: none; - border-color: transparent; - background: ${SystemColors.Field}; - color: ${SystemColors.FieldText}; - } - :host .content-region .expand-collapse-glyph { - fill: ${SystemColors.FieldText}; - } - :host .positioning-region:hover, - :host([selected]) .positioning-region { - background: ${SystemColors.Highlight}; - } - :host .positioning-region:hover .content-region, - :host([selected]) .positioning-region .content-region { - color: ${SystemColors.HighlightText}; - } - :host .positioning-region:hover .content-region .expand-collapse-glyph, - :host .positioning-region:hover .content-region .start, - :host .positioning-region:hover .content-region .end, - :host([selected]) .content-region .expand-collapse-glyph, - :host([selected]) .content-region .start, - :host([selected]) .content-region .end { - fill: ${SystemColors.HighlightText}; - } - :host([selected])::after { - background: ${SystemColors.Field}; - } - :host(:${focusVisible}) .positioning-region { - border-color: ${SystemColors.FieldText}; - box-shadow: 0 0 0 2px inset ${SystemColors.Field}; - color: ${SystemColors.FieldText}; - } - :host([disabled]) .content-region, - :host([disabled]) .positioning-region:hover .content-region { - opacity: 1; - color: ${SystemColors.GrayText}; - } - :host([disabled]) .content-region .expand-collapse-glyph, - :host([disabled]) .content-region .start, - :host([disabled]) .content-region .end, - :host([disabled]) - .positioning-region:hover - .content-region - .expand-collapse-glyph, - :host([disabled]) .positioning-region:hover .content-region .start, - :host([disabled]) .positioning-region:hover .content-region .end { - fill: ${SystemColors.GrayText}; - } - :host([disabled]) .positioning-region:hover { - background: ${SystemColors.Field}; - } - .expand-collapse-glyph, - .start, - .end { - fill: ${SystemColors.FieldText}; - } - :host(.nested) .expand-collapse-button:hover { - background: ${SystemColors.Field}; - } - :host(.nested) .expand-collapse-button:hover .expand-collapse-glyph { - fill: ${SystemColors.FieldText}; - } - `) - ); + :host(:focus) > .positioning-region { + outline: none; + } + + :host(:focus) .content-region { + outline: none; + } + + :host(:${focusVisible}) .positioning-region { + border: ${focusStrokeOuter} calc(${strokeWidth} * 1px) solid; + border-radius: calc(${controlCornerRadius} * 1px); + color: ${neutralForegroundRest}; + } + + .positioning-region { + display: flex; + position: relative; + box-sizing: border-box; + background: ${neutralFillStealthRest}; + border: transparent calc(${strokeWidth} * 1px) solid; + height: calc((${heightNumber} + 1) * 1px); + } + + .positioning-region::before { + content: ""; + display: block; + width: var(--tree-item-nested-width); + flex-shrink: 0; + } + + :host(:not([disabled])) .positioning-region:hover { + background: ${neutralFillStealthHover}; + } + + :host(:not([disabled])) .positioning-region:active { + background: ${neutralFillStealthActive}; + } + + .content-region { + display: inline-flex; + align-items: center; + white-space: nowrap; + width: 100%; + height: calc(${heightNumber} * 1px); + margin-inline-start: calc(${designUnit} * 2px + 8px); + font-size: ${typeRampBaseFontSize}; + line-height: ${typeRampBaseLineHeight}; + font-weight: 400; + } + + .items { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + font-size: calc(1em + (${designUnit} + 16) * 1px); + } + + .expand-collapse-button { + background: none; + border: none; + outline: none; + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: calc((${expandCollapseButtonSize} + (${designUnit} * 2)) * 1px); + height: calc((${expandCollapseButtonSize} + (${designUnit} * 2)) * 1px); + padding: 0; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + margin-left: 6px; + margin-right: 6px; + } + + .expand-collapse-glyph { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + transition: transform 0.1s linear; + + pointer-events: none; + fill: currentcolor; + } + + .start, + .end { + display: flex; + fill: currentcolor; + } + + ::slotted(svg) { + /* TODO: adaptive typography https://github.com/microsoft/fast/issues/2432 */ + width: 16px; + height: 16px; + } + + .start { + /* TODO: horizontalSpacing https://github.com/microsoft/fast/issues/2766 */ + margin-inline-end: calc(${designUnit} * 2px + 2px); + } + + .end { + /* TODO: horizontalSpacing https://github.com/microsoft/fast/issues/2766 */ + margin-inline-start: calc(${designUnit} * 2px + 2px); + } + + :host([expanded]) > .items { + animation: treeItemLoading ease-in 10ms; + animation-iteration-count: 1; + animation-fill-mode: forwards; + } + + :host([disabled]) .content-region { + opacity: ${disabledOpacity}; + cursor: ${disabledCursor}; + } + + :host(.nested) .content-region { + position: relative; + margin-inline-start: var(--expand-collapse-button-size); + } + + :host(.nested) .expand-collapse-button { + position: absolute; + } + + :host(.nested:not([disabled])) .expand-collapse-button:hover { + background: ${expandCollapseHoverBehavior}; + } + + :host([selected]) .positioning-region { + background: ${neutralFillRest}; + } + + :host([selected]:not([disabled])) .positioning-region:hover { + background: ${neutralFillHover}; + } + + :host([selected]:not([disabled])) .positioning-region:active { + background: ${neutralFillActive}; + } + + :host([selected]:not([disabled])) .expand-collapse-button:hover { + background: ${selectedExpandCollapseHoverBehavior}; + } + + :host([selected])::after { + /* The background needs to be calculated based on the selected background state + for this control. We currently have no way of changing that, so setting to + accent-foreground-rest for the time being */ + background: ${accentForegroundRest}; + border-radius: calc(${controlCornerRadius} * 1px); + content: ""; + display: block; + position: absolute; + top: calc((${heightNumber} / 4) * 1px); + width: 3px; + height: calc((${heightNumber} / 2) * 1px); + } + + ::slotted(${context.tagFor(TreeItem)}) { + --tree-item-nested-width: 1em; + --expand-collapse-button-nested-width: calc(${heightNumber} * -1px); + } + `.withBehaviors( + new DirectionalStyleSheetBehavior(ltr, rtl), + forcedColorsStylesheetBehavior( + css` + :host { + forced-color-adjust: none; + border-color: transparent; + background: ${SystemColors.Field}; + color: ${SystemColors.FieldText}; + } + :host .content-region .expand-collapse-glyph { + fill: ${SystemColors.FieldText}; + } + :host .positioning-region:hover, + :host([selected]) .positioning-region { + background: ${SystemColors.Highlight}; + } + :host .positioning-region:hover .content-region, + :host([selected]) .positioning-region .content-region { + color: ${SystemColors.HighlightText}; + } + :host .positioning-region:hover .content-region .expand-collapse-glyph, + :host .positioning-region:hover .content-region .start, + :host .positioning-region:hover .content-region .end, + :host([selected]) .content-region .expand-collapse-glyph, + :host([selected]) .content-region .start, + :host([selected]) .content-region .end { + fill: ${SystemColors.HighlightText}; + } + :host([selected])::after { + background: ${SystemColors.Field}; + } + :host(:${focusVisible}) .positioning-region { + border-color: ${SystemColors.FieldText}; + box-shadow: 0 0 0 2px inset ${SystemColors.Field}; + color: ${SystemColors.FieldText}; + } + :host([disabled]) .content-region, + :host([disabled]) .positioning-region:hover .content-region { + opacity: 1; + color: ${SystemColors.GrayText}; + } + :host([disabled]) .content-region .expand-collapse-glyph, + :host([disabled]) .content-region .start, + :host([disabled]) .content-region .end, + :host([disabled]) .positioning-region:hover .content-region .expand-collapse-glyph, + :host([disabled]) .positioning-region:hover .content-region .start, + :host([disabled]) .positioning-region:hover .content-region .end { + fill: ${SystemColors.GrayText}; + } + :host([disabled]) .positioning-region:hover { + background: ${SystemColors.Field}; + } + .expand-collapse-glyph, + .start, + .end { + fill: ${SystemColors.FieldText}; + } + :host(.nested) .expand-collapse-button:hover { + background: ${SystemColors.Field}; + } + :host(.nested) .expand-collapse-button:hover .expand-collapse-glyph { + fill: ${SystemColors.FieldText}; + } + ` + ) + ); diff --git a/packages/components/src/tree-view/index.ts b/packages/components/src/tree-view/index.ts index 9b386dfa..f2d150e0 100644 --- a/packages/components/src/tree-view/index.ts +++ b/packages/components/src/tree-view/index.ts @@ -1,11 +1,5 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - treeViewTemplate as template, - TreeView -} from '@microsoft/fast-foundation'; -import { treeViewStyles as styles } from '@microsoft/fast-components'; +import { treeViewTemplate as template, TreeView } from "@microsoft/fast-foundation"; +import { treeViewStyles as styles } from "./tree-view.styles.js"; /** * A function that returns a {@link @microsoft/fast-foundation#TreeView} registration for configuring the component with a DesignSystem. @@ -14,13 +8,13 @@ import { treeViewStyles as styles } from '@microsoft/fast-components'; * * @public * @remarks - * Generates HTML Element: `` + * Generates HTML Element: `` * */ -export const jpTreeView = TreeView.compose({ - baseName: 'tree-view', - template, - styles +export const fastTreeView = TreeView.compose({ + baseName: "tree-view", + template, + styles, }); /** diff --git a/packages/components/src/tree-view/tree-view.stories.ts b/packages/components/src/tree-view/tree-view.stories.ts index 9754f20e..0d93ab42 100644 --- a/packages/components/src/tree-view/tree-view.stories.ts +++ b/packages/components/src/tree-view/tree-view.stories.ts @@ -1,77 +1,8 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import type { StoryFn, Meta, StoryObj } from '@storybook/html'; -import { action } from '@storybook/addon-actions'; -import { setTheme } from '../utilities/storybook'; -import { TreeView } from './index'; +import TreeViewTemplate from "./fixtures/tree-view.html"; +import "./index.js"; export default { - title: 'Components/Tree View', - parameters: { - controls: { - disabled: true - } - } -} as Meta; - -const Template: StoryFn = (args, context): HTMLElement => { - const { - globals: { backgrounds, accent }, - parameters - } = context; - setTheme(accent, parameters.backgrounds, backgrounds); - const container = document.createElement('div'); - container.insertAdjacentHTML( - 'afterbegin', - ` - - Root item 1 - - Flowers - Daisy - Sunflower - - Rose - Pink - Red - White - - - Nested item 2 - Nested item 3 - - - Root item 2 - - Flowers - Daisy - Sunflower - Rose - - Nested item 2 - Nested item 3 - - - Root item 3 - - ` - ); - - const treeView = container.firstChild as TreeView; - - if (args.onExpand) { - treeView.addEventListener('expanded-change', args.onExpand); - } - if (args.onSelect) { - treeView.addEventListener('selected-change', args.onSelect); - } - - return treeView; + title: "Tree View", }; -export const Default: StoryObj = { render: Template.bind({}) }; -Default.args = { - onExpand: action('tree-item-expand'), - onSelect: action('tree-item-select') -}; +export const TreeView = () => TreeViewTemplate; diff --git a/packages/components/src/tree-view/tree-view.styles.ts b/packages/components/src/tree-view/tree-view.styles.ts new file mode 100644 index 00000000..b3bb9f3d --- /dev/null +++ b/packages/components/src/tree-view/tree-view.styles.ts @@ -0,0 +1,22 @@ +import { css, ElementStyles } from "@microsoft/fast-element"; +import { display, FoundationElementTemplate } from "@microsoft/fast-foundation"; + +/** + * Styles for Tree View + * @public + */ +export const treeViewStyles: FoundationElementTemplate = ( + context, + definition +) => css` + ${display("flex")} :host { + flex-direction: column; + align-items: stretch; + min-width: fit-content; + font-size: 0; + } + + :host:focus-visible { + outline: none; + } +`; diff --git a/packages/components/src/utilities/behaviors.ts b/packages/components/src/utilities/behaviors.ts new file mode 100644 index 00000000..ea877f1f --- /dev/null +++ b/packages/components/src/utilities/behaviors.ts @@ -0,0 +1,15 @@ +import { ElementStyles } from "@microsoft/fast-element"; +import { PropertyStyleSheetBehavior } from "@microsoft/fast-foundation"; + +/** + * Behavior that will conditionally apply a stylesheet based on the elements + * appearance property + * + * @param value - The value of the appearance property + * @param styles - The styles to be applied when condition matches + * + * @public + */ +export function appearanceBehavior(value: string, styles: ElementStyles) { + return new PropertyStyleSheetBehavior("appearance", value, styles); +}