diff --git a/.changeset/config.json b/.changeset/config.json index 4a0a1284e9..8877597ffd 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,8 @@ "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [], - "privatePackages": false + "privatePackages": false, + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } } diff --git a/.changeset/few-berries-yell.md b/.changeset/few-berries-yell.md deleted file mode 100644 index 37475a7f0e..0000000000 --- a/.changeset/few-berries-yell.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@salt-ds/icons": minor ---- - -Updated icons with arrows to be more consistent: - -- Inbox -- InboxSolid -- MoveAll -- MoveHorizontal -- MoveVertical - -Added: - -- MaximizeSolid -- Sparkle -- SparkleSolid diff --git a/.changeset/fresh-brooms-explain.md b/.changeset/fresh-brooms-explain.md new file mode 100644 index 0000000000..bf234a0498 --- /dev/null +++ b/.changeset/fresh-brooms-explain.md @@ -0,0 +1,9 @@ +--- +"@salt-ds/core": minor +--- + +Added support for multiple themes to be passed to `SaltProvider`, e.g., + +``` + +``` diff --git a/.changeset/hip-mirrors-attend.md b/.changeset/hip-mirrors-attend.md new file mode 100644 index 0000000000..ce723f7029 --- /dev/null +++ b/.changeset/hip-mirrors-attend.md @@ -0,0 +1,11 @@ +--- +"@salt-ds/lab": minor +--- + +Updated styling of date picker and calendar + +- Corner radius support for date picker panel in theme next +- Corner radius support for calendar selected days in theme next +- Use accent color for today indicator and highlight color in calendar + +Closes #3530. diff --git a/.changeset/silver-spiders-draw.md b/.changeset/silver-spiders-draw.md deleted file mode 100644 index ea68d844b0..0000000000 --- a/.changeset/silver-spiders-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@salt-ds/theme": patch ---- - -Updated status tokens for theme next to match latest design diff --git a/.changeset/stale-items-work.md b/.changeset/stale-items-work.md deleted file mode 100644 index 2b477a7cb4..0000000000 --- a/.changeset/stale-items-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@salt-ds/core": patch ---- - -Fixed the chevron alignment for multi-line accordions. diff --git a/.changeset/unlucky-cats-add.md b/.changeset/unlucky-cats-add.md new file mode 100644 index 0000000000..71702169e1 --- /dev/null +++ b/.changeset/unlucky-cats-add.md @@ -0,0 +1,5 @@ +--- +"@salt-ds/core": patch +--- + +Fixed `Tooltip` not having correct height. diff --git a/.github/workflows/build-storybook.yml b/.github/workflows/build-storybook.yml index 45e5a01d1b..61934ff316 100644 --- a/.github/workflows/build-storybook.yml +++ b/.github/workflows/build-storybook.yml @@ -5,6 +5,11 @@ on: - main pull_request: types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-storybook: runs-on: ubuntu-latest diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 858f03b03e..3d6e8b4ae4 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -8,6 +8,7 @@ on: jobs: chromatic-deployment: + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -60,7 +61,7 @@ jobs: run-id: ${{ github.event.workflow_run.id }} - name: Publish to Chromatic if: ${{ steps.chromatic_branch.outputs.draft != 'true' }} - uses: chromaui/action@v11 + uses: chromaui/action@v11.3.5 # Chromatic GitHub Action options with: # 👇 Chromatic projectToken, refer to the manage page to obtain it. diff --git a/.github/workflows/publish-storybook.yml b/.github/workflows/publish-storybook.yml index aeee08ff5c..10ad210fc3 100644 --- a/.github/workflows/publish-storybook.yml +++ b/.github/workflows/publish-storybook.yml @@ -8,6 +8,7 @@ on: jobs: deploy: + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9eba09ea8a..02f9a71d74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - release-ag-grid-theme-v1 permissions: contents: write diff --git a/.gitignore b/.gitignore index 579b2cc2b2..e50f1d4247 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ yarn-error.log* # Autogenerated CSS /docs/css/salt-core.css -/docs/css/salt-lab.css \ No newline at end of file +/docs/css/salt-lab.css +/docs/css/salt-countries.css \ No newline at end of file diff --git a/.storybook/manager.ts b/.storybook/manager.ts index f5f307e836..4bf4991609 100644 --- a/.storybook/manager.ts +++ b/.storybook/manager.ts @@ -1,6 +1,16 @@ -import { addons } from "@storybook/manager-api"; +import { addons, types } from "@storybook/manager-api"; import saltTheme from "./SaltTheme"; +import { ThemeNextToolbar } from "./toolbar/ThemeNextToolbar"; addons.setConfig({ theme: saltTheme, }); + +addons.register("theme-next-addon", () => { + addons.add("theme-next-addon/toolbar", { + title: "Theme next toolbar", + //👇 Sets the type of UI element in Storybook + type: types.TOOL, + render: ThemeNextToolbar, + }); +}); diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 40e16dab2f..db85e47e50 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -22,10 +22,12 @@ import { withResponsiveWrapper } from "docs/decorators/withResponsiveWrapper"; import { WithTextSpacingWrapper } from "docs/decorators/withTextSpacingWrapper"; import { withScaffold } from "docs/decorators/withScaffold"; import { withDateMock } from "docs/decorators/withDateMock"; -import { SaltProvider } from "@salt-ds/core"; +import { SaltProvider, UNSTABLE_SaltProviderNext } from "@salt-ds/core"; import { DocsContainer } from "@storybook/addon-docs"; import { initialize, mswLoader } from "msw-storybook-addon"; +import { globalOptions as themeNextGlobals } from "./toolbar/ThemeNextToolbar"; + const densities = ["touch", "low", "medium", "high"]; const DEFAULT_DENSITY = "medium"; const DEFAULT_MODE = "light"; @@ -107,47 +109,7 @@ export const globalTypes: GlobalTypes = { title: "Component Style Injection", }, }, - themeNext: { - name: "Experimental theme next", - description: "Turn on/off theme next", - defaultValue: "disable", - toolbar: { - icon: "beaker", - items: ["disable", "enable"], - title: "Theme Next", - }, - }, - corner: { - name: "Experimental corner", - description: "Switch corner to sharp / rounded", - defaultValue: "sharp", - // if: { global: "themeNext", eq: "enable" }, // todo: why if doesn't work? - toolbar: { - icon: "beaker", - items: ["sharp", "rounded"], - title: "Corner", - }, - }, - headingFont: { - name: "Experimental heading font", - description: "Switch heading font to open sans / amplitude", - defaultValue: "Open Sans", - toolbar: { - icon: "beaker", - items: ["Open Sans", "Amplitude"], - title: "Heading font", - }, - }, - accent: { - name: "Experimental accent", - description: "Switch accent to blue / teal", - defaultValue: "blue", - toolbar: { - icon: "beaker", - items: ["blue", "teal"], - title: "Accent", - }, - }, + ...themeNextGlobals, }; export const argTypes: ArgTypes = { @@ -185,20 +147,29 @@ export const parameters: Parameters = { children, context, ...rest - }: ComponentProps) => ( - - ) => { + const ChosenProvider = + /* @ts-ignore Waiting for https://github.com/storybookjs/storybook/issues/12982 */ + context.store.globals.globals?.themeNext === "enable" + ? UNSTABLE_SaltProviderNext + : SaltProvider; + return ( + + - {children} - - - ), + mode={context.store.globals.globals?.mode} + enableStyleInjection={ + /* @ts-ignore Waiting for https://github.com/storybookjs/storybook/issues/12982 */ + context.store.globals.globals?.styleInjection === "enable" + } + /* @ts-ignore Waiting for https://github.com/storybookjs/storybook/issues/12982 */ + accent={context.store.globals.globals?.accent} + > + {children} + + + ); + }, }, // disables snapshotting on a global level chromatic: { disableSnapshot: true }, diff --git a/.storybook/toolbar/ThemeNextToolbar.css b/.storybook/toolbar/ThemeNextToolbar.css new file mode 100644 index 0000000000..d1c18499c9 --- /dev/null +++ b/.storybook/toolbar/ThemeNextToolbar.css @@ -0,0 +1,9 @@ +/* Custom Toolbar */ +.theme-next-toolbar-group-wrapper { + cursor: not-allowed; +} + +.theme-next-toolbar-group-wrapper > span > span { + font-weight: bold; + color: darkgray; +} diff --git a/.storybook/toolbar/ThemeNextToolbar.tsx b/.storybook/toolbar/ThemeNextToolbar.tsx new file mode 100644 index 0000000000..5250546924 --- /dev/null +++ b/.storybook/toolbar/ThemeNextToolbar.tsx @@ -0,0 +1,120 @@ +import type { TooltipLinkListLink } from "@storybook/components"; +import { + IconButton, + Separator, + TooltipLinkList, + WithTooltip, +} from "@storybook/components"; +import { BeakerIcon, CheckIcon } from "@storybook/icons"; +import { useGlobals } from "@storybook/manager-api"; +import { clsx } from "clsx"; +import React, { AnchorHTMLAttributes } from "react"; + +import "./ThemeNextToolbar.css"; + +const description = "Theme next controls"; + +const camelCaseToWords = (s: string) => { + const result = s.replace(/([A-Z])/g, " $1"); + return result.charAt(0).toUpperCase() + result.slice(1); +}; + +export const globalOptions: Record< + string, + { name: string; description: string; defaultValue: string; items: string[] } +> = { + themeNext: { + name: "Experimental theme next", + description: "Turn on/off theme next", + defaultValue: "disable", + items: ["enable", "disable"], + }, + corner: { + name: "Experimental corner", + description: "Switch corner to sharp / rounded", + defaultValue: "sharp", + items: ["sharp", "rounded"], + }, + headingFont: { + name: "Experimental heading font", + description: "Switch heading font to open sans / amplitude", + defaultValue: "Open Sans", + items: ["Open Sans", "Amplitude"], + }, + accent: { + name: "Experimental accent", + description: "Switch accent to blue / teal", + defaultValue: "blue", + items: ["blue", "teal"], + }, + actionFont: { + name: "Experimental action font", + description: "Switch action font to open sans / amplitude", + defaultValue: "Open Sans", + items: ["Open Sans", "Amplitude"], + }, +}; + +const GroupWrapper = ({ + className, + children, +}: AnchorHTMLAttributes) => { + return ( +
+ ); +}; + +export const ThemeNextToolbar = ({ active }: { active?: boolean }) => { + const [globals, updateGlobals] = useGlobals(); + + const items: TooltipLinkListLink[] = Object.keys(globalOptions).flatMap( + (globalKey) => { + return [ + { + id: `theme-next-${globalKey}-header`, + title: camelCaseToWords(globalKey), + LinkWrapper: GroupWrapper, // Custom wrapper to render group + href: "#", // Without href, `LinkWrapper` will not work + }, + ...globalOptions[globalKey].items.map((value) => { + const disabled = + globalKey === "themeNext" + ? false + : globals["themeNext"] !== "enable"; + const active = globals[globalKey] === value; + + return { + id: `theme-next-${globalKey}-${value}`, + right: active ? ( + + ) : undefined, + active, + title: camelCaseToWords(value), + onClick: () => { + !disabled && updateGlobals({ [globalKey]: value }); + }, + disabled, + }; + }), + ]; + } + ); + + return ( + <> + + } + trigger="click" + closeOnOutsideClick + > + + Theme Next + + + + ); +}; diff --git a/.yarn/patches/@changesets-assemble-release-plan-npm-5.2.2-11f5894b70.patch b/.yarn/patches/@changesets-assemble-release-plan-npm-5.2.2-11f5894b70.patch deleted file mode 100644 index a058ad34b8..0000000000 --- a/.yarn/patches/@changesets-assemble-release-plan-npm-5.2.2-11f5894b70.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git a/dist/assemble-release-plan.cjs.dev.js b/dist/assemble-release-plan.cjs.dev.js -index 3a37c62c975518f975c22e1b4b3974d9b325a5da..8c2b0b2cb13e714faa6e2e52065dd92adedd9b43 100644 ---- a/dist/assemble-release-plan.cjs.dev.js -+++ b/dist/assemble-release-plan.cjs.dev.js -@@ -65,6 +65,9 @@ function incrementVersion(release, preInfo) { - } - - let version = semver.inc(release.oldVersion, release.type); -+ if (release.name === "@salt-ds/lab" || release.name === "@salt-ds/data-grid") { -+ version = semver.inc(release.oldVersion, 'prerelease'); -+ } - - if (preInfo !== undefined && preInfo.state.mode !== "exit") { - let preVersion = preInfo.preVersions.get(release.name); -diff --git a/dist/assemble-release-plan.cjs.prod.js b/dist/assemble-release-plan.cjs.prod.js -index 87b4c104bf3fa53ba498ced6f81eda0ed4c86436..720216128636871d9e20967ea074189682fa672a 100644 ---- a/dist/assemble-release-plan.cjs.prod.js -+++ b/dist/assemble-release-plan.cjs.prod.js -@@ -49,6 +49,9 @@ function _objectSpread2(target) { - function incrementVersion(release, preInfo) { - if ("none" === release.type) return release.oldVersion; - let version = semver.inc(release.oldVersion, release.type); -+ if (release.name === "@salt-ds/lab" || release.name === "@salt-ds/data-grid") { -+ version = semver.inc(release.oldVersion, 'prerelease'); -+ } - if (void 0 !== preInfo && "exit" !== preInfo.state.mode) { - let preVersion = preInfo.preVersions.get(release.name); - if (void 0 === preVersion) throw new errors.InternalError(`preVersion for ${release.name} does not exist when preState is defined`); diff --git a/.yarn/patches/@changesets-assemble-release-plan-npm-6.0.2-d74b7b2762.patch b/.yarn/patches/@changesets-assemble-release-plan-npm-6.0.2-d74b7b2762.patch new file mode 100644 index 0000000000..00b9d78f24 --- /dev/null +++ b/.yarn/patches/@changesets-assemble-release-plan-npm-6.0.2-d74b7b2762.patch @@ -0,0 +1,46 @@ +diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js +index 60427457c887f2d72168fecec83d79088c68e3a4..b2f9655ac07d6adbc29b5c7e46c5d7af7ba1eba5 100644 +--- a/dist/changesets-assemble-release-plan.cjs.js ++++ b/dist/changesets-assemble-release-plan.cjs.js +@@ -111,6 +111,9 @@ function incrementVersion(release, preInfo) { + } + + let version = semverInc__default["default"](release.oldVersion, release.type); ++ if (release.name === "@salt-ds/lab") { ++ version = semverInc__default["default"](release.oldVersion, "prerelease") ++ } + + if (preInfo !== undefined && preInfo.state.mode !== "exit") { + let preVersion = preInfo.preVersions.get(release.name); +@@ -299,7 +302,7 @@ function shouldBumpMajor({ + // we check if it is a peerDependency because if it is, our dependent bump type might need to be major. + return depType === "peerDependencies" && nextRelease.type !== "none" && nextRelease.type !== "patch" && ( // 1. If onlyUpdatePeerDependentsWhenOutOfRange set to true, bump major if the version is leaving the range. + // 2. If onlyUpdatePeerDependentsWhenOutOfRange set to false, bump major regardless whether or not the version is leaving the range. +- !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange)) && ( // bump major only if the dependent doesn't already has a major release. ++ !onlyUpdatePeerDependentsWhenOutOfRange) && ( // bump major only if the dependent doesn't already has a major release. (https://github.com/changesets/changesets/issues/1011) + !releases.has(dependent) || releases.has(dependent) && releases.get(dependent).type !== "major"); + } + +diff --git a/dist/changesets-assemble-release-plan.esm.js b/dist/changesets-assemble-release-plan.esm.js +index f6583cf3f639e1fe4df764a015689dea74127236..318ecb08e2c58e8d3ac4ef11f659a5cd09e9e66e 100644 +--- a/dist/changesets-assemble-release-plan.esm.js ++++ b/dist/changesets-assemble-release-plan.esm.js +@@ -100,6 +100,9 @@ function incrementVersion(release, preInfo) { + } + + let version = semverInc(release.oldVersion, release.type); ++ if (release.name === "@salt-ds/lab") { ++ version = semverInc(release.oldVersion, "prerelease"); ++ } + + if (preInfo !== undefined && preInfo.state.mode !== "exit") { + let preVersion = preInfo.preVersions.get(release.name); +@@ -288,7 +291,7 @@ function shouldBumpMajor({ + // we check if it is a peerDependency because if it is, our dependent bump type might need to be major. + return depType === "peerDependencies" && nextRelease.type !== "none" && nextRelease.type !== "patch" && ( // 1. If onlyUpdatePeerDependentsWhenOutOfRange set to true, bump major if the version is leaving the range. + // 2. If onlyUpdatePeerDependentsWhenOutOfRange set to false, bump major regardless whether or not the version is leaving the range. +- !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange)) && ( // bump major only if the dependent doesn't already has a major release. ++ !onlyUpdatePeerDependentsWhenOutOfRange) && ( // bump major only if the dependent doesn't already has a major release. (https://github.com/changesets/changesets/issues/1011) + !releases.has(dependent) || releases.has(dependent) && releases.get(dependent).type !== "major"); + } + diff --git a/.yarn/patches/@jpmorganchase-mosaic-content-editor-plugin-npm-0.1.0-beta.65-5b63b22b12.patch b/.yarn/patches/@jpmorganchase-mosaic-content-editor-plugin-npm-0.1.0-beta.65-5b63b22b12.patch deleted file mode 100644 index b6e8af020a..0000000000 --- a/.yarn/patches/@jpmorganchase-mosaic-content-editor-plugin-npm-0.1.0-beta.65-5b63b22b12.patch +++ /dev/null @@ -1,492 +0,0 @@ -diff --git a/dist/chunk-CXTIKD2Y.js b/dist/chunk-CXTIKD2Y.js -index 99ea36a9ecfd5fb9532400a95637a4eceeb588ef..c33e2b9cb3b5c9a477f26d89834afa1645fa545e 100644 ---- a/dist/chunk-CXTIKD2Y.js -+++ b/dist/chunk-CXTIKD2Y.js -@@ -1 +1 @@ --import{a as b}from"./chunk-KZXCNQER.js";import{a as I}from"./chunk-YBFKG74H.js";import{a as h}from"./chunk-XSZUCLBY.js";import{a as v}from"./chunk-3QIETKQW.js";import e,{useState as i}from"react";import{useLexicalComposerContext as M}from"@lexical/react/LexicalComposerContext";import{Button as c,Icon as P}from"@jpmorganchase/mosaic-components";import{string as C,object as V}from"yup";import{Input as S,FormField as D,FormFieldLabel as y,FormFieldHelperText as A}from"@salt-ds/core";import{ButtonBar as q,DialogTitle as w,DialogContent as L,DialogActions as N}from"@salt-ds/lab";var B=V({alt:C().required("Alternative Information is a required field").max(100,"Alternative Information must be fewer than 100 characters"),url:C().required("Url is required").url("Must be a valid Url")}),E={url:"https://"},Y=()=>{let[T]=M(),[s,m]=i(!1),[r,u]=i(E),[n,d]=i(),f=t=>{m(t),t||u(E)},x=()=>m(!0),p=()=>{f(!1)},g=t=>{let o=t.inner.reduce((l,{path:a,message:O})=>({...l,[a]:O}),{});d(o)},F=t=>{let{name:o,value:l}=t.target,a={...r,[o]:l};B.validateAt(o,a,{abortEarly:!1}).then(()=>{d({...n,[o]:void 0})},g),u(a)};return e.createElement(e.Fragment,null,e.createElement(v,{active:s,onClick:x,label:"Insert Image"},e.createElement(P,{name:"addDocument"})),e.createElement(h,{onOpenChange:f,open:s},e.createElement("form",{onSubmit:async t=>{t.preventDefault(),B.validate(r,{abortEarly:!1}).then(()=>{let o={alt:r&&r.alt!==void 0?r.alt:null,url:r&&r.url!==void 0?r.url:null};T.dispatchCommand(I,o),p()},g)},noValidate:!0},e.createElement(w,null,"Insert Image"),e.createElement(L,null,e.createElement("div",{className:b.fullWidth},e.createElement(D,{validationStatus:n?.url?"error":void 0},e.createElement(y,null,"Url for image"),e.createElement(S,{value:r?.url,inputProps:{name:"url"},onChange:F}),e.createElement(A,null,n?.url)),e.createElement(D,{validationStatus:n?.alt?"error":void 0},e.createElement(y,null,"Alternative Information (alt)"),e.createElement(S,{value:r?.alt,inputProps:{name:"alt"},onChange:F}),e.createElement(A,null,n?.alt||"Provides alternative information for the image if for some reason it cannot be viewed")))),e.createElement(N,null,e.createElement(q,null,e.createElement(c,{onClick:p},"Cancel"),e.createElement(c,{variant:"cta",type:"submit"},"Insert"))))))};export{Y as a}; -+import{a as b}from"./chunk-KZXCNQER.js";import{a as I}from"./chunk-YBFKG74H.js";import{a as h}from"./chunk-XSZUCLBY.js";import{a as v}from"./chunk-3QIETKQW.js";import e,{useState as i}from"react";import{useLexicalComposerContext as M}from"@lexical/react/LexicalComposerContext";import{Button as c,Icon as P}from"@jpmorganchase/mosaic-components";import{string as C,object as V}from"yup";import{Input as S,FormField as D,FormFieldLabel as y,FormFieldHelperText as A, DialogHeader as w,DialogContent as L,DialogActions as N}from"@salt-ds/core";import{ButtonBar as q}from"@salt-ds/lab";var B=V({alt:C().required("Alternative Information is a required field").max(100,"Alternative Information must be fewer than 100 characters"),url:C().required("Url is required").url("Must be a valid Url")}),E={url:"https://"},Y=()=>{let[T]=M(),[s,m]=i(!1),[r,u]=i(E),[n,d]=i(),f=t=>{m(t),t||u(E)},x=()=>m(!0),p=()=>{f(!1)},g=t=>{let o=t.inner.reduce((l,{path:a,message:O})=>({...l,[a]:O}),{});d(o)},F=t=>{let{name:o,value:l}=t.target,a={...r,[o]:l};B.validateAt(o,a,{abortEarly:!1}).then(()=>{d({...n,[o]:void 0})},g),u(a)};return e.createElement(e.Fragment,null,e.createElement(v,{active:s,onClick:x,label:"Insert Image"},e.createElement(P,{name:"addDocument"})),e.createElement(h,{onOpenChange:f,open:s},e.createElement("form",{onSubmit:async t=>{t.preventDefault(),B.validate(r,{abortEarly:!1}).then(()=>{let o={alt:r&&r.alt!==void 0?r.alt:null,url:r&&r.url!==void 0?r.url:null};T.dispatchCommand(I,o),p()},g)},noValidate:!0},e.createElement(w,null,"Insert Image"),e.createElement(L,null,e.createElement("div",{className:b.fullWidth},e.createElement(D,{validationStatus:n?.url?"error":void 0},e.createElement(y,null,"Url for image"),e.createElement(S,{value:r?.url,inputProps:{name:"url"},onChange:F}),e.createElement(A,null,n?.url)),e.createElement(D,{validationStatus:n?.alt?"error":void 0},e.createElement(y,null,"Alternative Information (alt)"),e.createElement(S,{value:r?.alt,inputProps:{name:"alt"},onChange:F}),e.createElement(A,null,n?.alt||"Provides alternative information for the image if for some reason it cannot be viewed")))),e.createElement(N,null,e.createElement(q,null,e.createElement(c,{onClick:p},"Cancel"),e.createElement(c,{variant:"cta",type:"submit"},"Insert"))))))};export{Y as a}; -diff --git a/dist/chunk-EPOQVSZ5.js b/dist/chunk-EPOQVSZ5.js -index 604121a0a7b13c2ea6b923aed2e4b270cd81d8a9..8485f51a4202ab077516e3de61ab0628ce7b87cf 100644 ---- a/dist/chunk-EPOQVSZ5.js -+++ b/dist/chunk-EPOQVSZ5.js -@@ -1 +1 @@ --import{a as C}from"./chunk-WZYDYWT3.js";import{a as k}from"./chunk-XSZUCLBY.js";import{a as I}from"./chunk-3QIETKQW.js";import{e as p}from"./chunk-G57W376H.js";import{$createLinkNode as W,TOGGLE_LINK_COMMAND as G}from"@lexical/link";import{useLexicalComposerContext as U}from"@lexical/react/LexicalComposerContext";import{$wrapNodes as j}from"@lexical/selection";import{$createParagraphNode as Y,$createTextNode as z,$getSelection as J,$isRangeSelection as Q,$isTextNode as X,COMMAND_PRIORITY_EDITOR as Z,createCommand as R}from"lexical";import ee,{useCallback as te,useEffect as ne}from"react";import e,{useEffect as O,useState as F}from"react";import{useLexicalComposerContext as B}from"@lexical/react/LexicalComposerContext";import{Button as N,Icon as _}from"@jpmorganchase/mosaic-components";import{string as h,object as A}from"yup";import{Input as T,FormField as D,FormFieldLabel as S,FormFieldHelperText as E}from"@salt-ds/core";import{ButtonBar as $,DialogTitle as w,DialogContent as K,DialogActions as q}from"@salt-ds/lab";import{$getSelection as V,$isRangeSelection as H}from"lexical";var b=A({url:h().required("Url is required"),text:h().required("Text is required").max(100,"Text must be fewer than 100 characters")}),Le=()=>{let{isInsertingLink:t,setIsInsertingLink:r}=p();return e.createElement(I,{active:t,onClick:()=>r(!0),label:"Insert Link"},e.createElement(_,{name:"linked"}))},v={url:"https://",text:""},y=()=>{let[t]=B(),{isInsertingLink:r,setIsInsertingLink:s}=p(),[o,i]=F(v),[a,d]=F(),m=n=>{s(n),n||(d(void 0),i(v))},g=()=>{m(!1)};O(()=>{t.getEditorState().read(()=>{if(r){let n=V();if(H(n)){let l=n.getTextContent();i(u=>({...u,text:l}))}}})},[t,r]);let x=n=>{let l=n.inner.reduce((u,{path:c,message:P})=>({...u,[c]:P}),{});d(l)},L=n=>{let{name:l,value:u}=n.target,c={...o,[l]:u};b.validateAt(l,c,{abortEarly:!1}).then(()=>{d({...a,[l]:void 0})},x),i(c)},M=async()=>{b.validate(o,{abortEarly:!1}).then(()=>{let n={url:o?.url,text:o?.text};t.dispatchCommand(f,n),g()},x)};return e.createElement(k,{onOpenChange:m,open:r},e.createElement(w,null,"Insert Link"),e.createElement(K,null,e.createElement("div",{className:C.fullWidth},e.createElement(D,{validationStatus:a?.url?"error":void 0},e.createElement(S,null,"Image URL text"),e.createElement(T,{value:o?.url,inputProps:{name:"url"},onChange:L}),e.createElement(E,null,a?.url)),e.createElement(D,{validationStatus:a?.text?"error":void 0},e.createElement(S,null,"Link Text"),e.createElement(T,{value:o?.text,inputProps:{name:"text"},onChange:L}),e.createElement(E,null,a?.text)))),e.createElement(q,null,e.createElement($,null,e.createElement(N,{onClick:g},"Cancel"),e.createElement(N,{variant:"cta",onClick:M},"Insert"))))};var f=R();function oe(){let[t]=U(),r=te(({url:s,text:o})=>{t.update(()=>{let i=J();if(Q(i)&&s!==void 0&&o!==void 0){let a=i.focus.getNode();if(X(a))t.dispatchCommand(G,s);else{let d=W(s),m=z(o);m.setFormat(i.focus.getNode().getFormat()),d.append(m),j(i,()=>d,Y())}}})},[t]);ne(()=>t.registerCommand(f,s=>(r(s),!0),Z),[t,r])}function Se(){return oe(),ee.createElement(y,null)}export{f as a,Se as b,Le as c,y as d}; -+import{a as C}from"./chunk-WZYDYWT3.js";import{a as k}from"./chunk-XSZUCLBY.js";import{a as I}from"./chunk-3QIETKQW.js";import{e as p}from"./chunk-G57W376H.js";import{$createLinkNode as W,TOGGLE_LINK_COMMAND as G}from"@lexical/link";import{useLexicalComposerContext as U}from"@lexical/react/LexicalComposerContext";import{$wrapNodes as j}from"@lexical/selection";import{$createParagraphNode as Y,$createTextNode as z,$getSelection as J,$isRangeSelection as Q,$isTextNode as X,COMMAND_PRIORITY_EDITOR as Z,createCommand as R}from"lexical";import ee,{useCallback as te,useEffect as ne}from"react";import e,{useEffect as O,useState as F}from"react";import{useLexicalComposerContext as B}from"@lexical/react/LexicalComposerContext";import{Button as N,Icon as _}from"@jpmorganchase/mosaic-components";import{string as h,object as A}from"yup";import{Input as T,FormField as D,FormFieldLabel as S,FormFieldHelperText as E, DialogHeader as w,DialogContent as K,DialogActions as q}from"@salt-ds/core";import{ButtonBar as $}from"@salt-ds/lab";import{$getSelection as V,$isRangeSelection as H}from"lexical";var b=A({url:h().required("Url is required"),text:h().required("Text is required").max(100,"Text must be fewer than 100 characters")}),Le=()=>{let{isInsertingLink:t,setIsInsertingLink:r}=p();return e.createElement(I,{active:t,onClick:()=>r(!0),label:"Insert Link"},e.createElement(_,{name:"linked"}))},v={url:"https://",text:""},y=()=>{let[t]=B(),{isInsertingLink:r,setIsInsertingLink:s}=p(),[o,i]=F(v),[a,d]=F(),m=n=>{s(n),n||(d(void 0),i(v))},g=()=>{m(!1)};O(()=>{t.getEditorState().read(()=>{if(r){let n=V();if(H(n)){let l=n.getTextContent();i(u=>({...u,text:l}))}}})},[t,r]);let x=n=>{let l=n.inner.reduce((u,{path:c,message:P})=>({...u,[c]:P}),{});d(l)},L=n=>{let{name:l,value:u}=n.target,c={...o,[l]:u};b.validateAt(l,c,{abortEarly:!1}).then(()=>{d({...a,[l]:void 0})},x),i(c)},M=async()=>{b.validate(o,{abortEarly:!1}).then(()=>{let n={url:o?.url,text:o?.text};t.dispatchCommand(f,n),g()},x)};return e.createElement(k,{onOpenChange:m,open:r},e.createElement(w,null,"Insert Link"),e.createElement(K,null,e.createElement("div",{className:C.fullWidth},e.createElement(D,{validationStatus:a?.url?"error":void 0},e.createElement(S,null,"Image URL text"),e.createElement(T,{value:o?.url,inputProps:{name:"url"},onChange:L}),e.createElement(E,null,a?.url)),e.createElement(D,{validationStatus:a?.text?"error":void 0},e.createElement(S,null,"Link Text"),e.createElement(T,{value:o?.text,inputProps:{name:"text"},onChange:L}),e.createElement(E,null,a?.text)))),e.createElement(q,null,e.createElement($,null,e.createElement(N,{onClick:g},"Cancel"),e.createElement(N,{variant:"cta",onClick:M},"Insert"))))};var f=R();function oe(){let[t]=U(),r=te(({url:s,text:o})=>{t.update(()=>{let i=J();if(Q(i)&&s!==void 0&&o!==void 0){let a=i.focus.getNode();if(X(a))t.dispatchCommand(G,s);else{let d=W(s),m=z(o);m.setFormat(i.focus.getNode().getFormat()),d.append(m),j(i,()=>d,Y())}}})},[t]);ne(()=>t.registerCommand(f,s=>(r(s),!0),Z),[t,r])}function Se(){return oe(),ee.createElement(y,null)}export{f as a,Se as b,Le as c,y as d}; -diff --git a/dist/chunk-LQBKKB6A.js b/dist/chunk-LQBKKB6A.js -index a872ecad696aaca107d28071e8d782b130fca59b..8eaee07dbc962ece98776a1c416a9ba010287827 100644 ---- a/dist/chunk-LQBKKB6A.js -+++ b/dist/chunk-LQBKKB6A.js -@@ -1 +1 @@ --import{a as k}from"./chunk-VDGHHU3F.js";import{a as y}from"./chunk-R2MEEU32.js";import{a as P}from"./chunk-M4ISRNFD.js";import{a as b}from"./chunk-3LGZ7Z7G.js";import{a as D}from"./chunk-XSZUCLBY.js";import{c as C,d as S}from"./chunk-G57W376H.js";import e,{useState as g}from"react";import F from"md5";import{useLexicalComposerContext as G}from"@lexical/react/LexicalComposerContext";import{$convertToMarkdownString as O}from"@lexical/markdown";import{Link as $,P2 as w,Button as v}from"@jpmorganchase/mosaic-components";import{ButtonBar as J,DialogTitle as U,DialogContent as V,DialogActions as _}from"@salt-ds/lab";var j=({isRaising:f,prHref:l,error:c})=>!f&&!l&&!c?e.createElement(e.Fragment,null,e.createElement(w,null,"The content of this page resides in a Git repository and to update it requires a Pull Request which will be reviewed by the content owners."),e.createElement("br",null),e.createElement(w,null,"Should you decide to stop editing before creating the Pull Request then all changes will be lost.")):null,le=({meta:f,persistUrl:l})=>{let{pageState:c,setPageState:B}=C(),{user:p}=S(),[q]=G(),[s,t]=g(!1),[r,n]=g(null),[i,m]=g(null),[E,a]=g([]),d=c==="SAVING",I=r!==null?"success":"info",h=o=>{t(o),o||(B("EDIT"),n(null),a([]))},M=()=>{h(!1)},x=o=>{m(o||"Sorry - an unexpected error has occurred"),n(null),a([]),t(!1)},T=o=>{n(o.message?.links?.self[0]?.href),t(!1)},A=o=>{a(u=>[...u,o])},{sendWorkflowProgressMessage:H}=b(d,x,A,T),L=()=>{t(!0),n(null),m(null);try{q.update(()=>{let o=O(P);if(o&&p&&l){let{sid:u,displayName:N,email:W}=p;H(JSON.stringify({user:{sid:u,name:N,email:W},route:f.route,markdown:o,name:"save"}),F(`${u.toLowerCase()} - save`))}})}catch{m("Sorry - an unexpected error has occurred"),n(null),t(!1),a([])}};return e.createElement(D,{onOpenChange:h,open:d,status:i?"error":I},e.createElement(U,{className:y.title},r?"Pull Request Created Successfully":"Save Changes"),e.createElement(V,null,(s||i)&&!r&&e.createElement(k,{error:i,progress:E}),e.createElement(j,{isRaising:s,prHref:r,error:i}),!s&&r&&e.createElement($,{href:r,target:"_blank"},"A Pull Request for your changes has been created")),e.createElement(_,null,e.createElement(J,null,e.createElement(v,{disabled:s,onClick:M},r?"Done":"Cancel"),e.createElement(v,{disabled:l===void 0||s||r!==null,onClick:L,variant:"cta"},"Raise Pull Request"))))};export{le as a}; -+import{a as k}from"./chunk-VDGHHU3F.js";import{a as y}from"./chunk-R2MEEU32.js";import{a as P}from"./chunk-M4ISRNFD.js";import{a as b}from"./chunk-3LGZ7Z7G.js";import{a as D}from"./chunk-XSZUCLBY.js";import{c as C,d as S}from"./chunk-G57W376H.js";import e,{useState as g}from"react";import F from"md5";import{useLexicalComposerContext as G}from"@lexical/react/LexicalComposerContext";import{$convertToMarkdownString as O}from"@lexical/markdown";import{Link as $,P2 as w,Button as v}from"@jpmorganchase/mosaic-components";import{DialogHeader as U,DialogContent as V,DialogActions as _}from"@salt-ds/core";import{ButtonBar as J}from"@salt-ds/lab";var j=({isRaising:f,prHref:l,error:c})=>!f&&!l&&!c?e.createElement(e.Fragment,null,e.createElement(w,null,"The content of this page resides in a Git repository and to update it requires a Pull Request which will be reviewed by the content owners."),e.createElement("br",null),e.createElement(w,null,"Should you decide to stop editing before creating the Pull Request then all changes will be lost.")):null,le=({meta:f,persistUrl:l})=>{let{pageState:c,setPageState:B}=C(),{user:p}=S(),[q]=G(),[s,t]=g(!1),[r,n]=g(null),[i,m]=g(null),[E,a]=g([]),d=c==="SAVING",I=r!==null?"success":"info",h=o=>{t(o),o||(B("EDIT"),n(null),a([]))},M=()=>{h(!1)},x=o=>{m(o||"Sorry - an unexpected error has occurred"),n(null),a([]),t(!1)},T=o=>{n(o.message?.links?.self[0]?.href),t(!1)},A=o=>{a(u=>[...u,o])},{sendWorkflowProgressMessage:H}=b(d,x,A,T),L=()=>{t(!0),n(null),m(null);try{q.update(()=>{let o=O(P);if(o&&p&&l){let{sid:u,displayName:N,email:W}=p;H(JSON.stringify({user:{sid:u,name:N,email:W},route:f.route,markdown:o,name:"save"}),F(`${u.toLowerCase()} - save`))}})}catch{m("Sorry - an unexpected error has occurred"),n(null),t(!1),a([])}};return e.createElement(D,{onOpenChange:h,open:d,status:i?"error":I},e.createElement(U,{className:y.title},r?"Pull Request Created Successfully":"Save Changes"),e.createElement(V,null,(s||i)&&!r&&e.createElement(k,{error:i,progress:E}),e.createElement(j,{isRaising:s,prHref:r,error:i}),!s&&r&&e.createElement($,{href:r,target:"_blank"},"A Pull Request for your changes has been created")),e.createElement(_,null,e.createElement(J,null,e.createElement(v,{disabled:s,onClick:M},r?"Done":"Cancel"),e.createElement(v,{disabled:l===void 0||s||r!==null,onClick:L,variant:"cta"},"Raise Pull Request"))))};export{le as a}; -diff --git a/dist/chunk-XSZUCLBY.js b/dist/chunk-XSZUCLBY.js -index 5dd767ac61f2f900c9fcc82099003c455de450a0..164a3acb5d60453fa2345263659c19bf66399f9b 100644 ---- a/dist/chunk-XSZUCLBY.js -+++ b/dist/chunk-XSZUCLBY.js -@@ -1 +1 @@ --import{a as o}from"./chunk-OW5SK4AJ.js";import s from"react";import{Dialog as t}from"@salt-ds/lab";import{themeClassName as l}from"@jpmorganchase/mosaic-theme";import i from"clsx";var f=({className:a,...r})=>s.createElement(t,{className:i(l,o.root,a),...r});export{f as a}; -+import{a as o}from"./chunk-OW5SK4AJ.js";import s from"react";import{Dialog as t}from"@salt-ds/core";import{themeClassName as l}from"@jpmorganchase/mosaic-theme";import i from"clsx";var f=({className:a,...r})=>s.createElement(t,{className:i(l,o.root,a),...r});export{f as a}; -diff --git a/dist/components/Dialog.d.ts b/dist/components/Dialog.d.ts -index 30d9d0bb361dc7c0c2472f7c972ad5413749d921..f0c3535c15da044bceda8a268e1768227ef3bd46 100644 ---- a/dist/components/Dialog.d.ts -+++ b/dist/components/Dialog.d.ts -@@ -1,5 +1,5 @@ - /// --import { type DialogProps as SaltDialogProps } from '@salt-ds/lab'; -+import { type DialogProps as SaltDialogProps } from '@salt-ds/core'; - interface DialogProps extends SaltDialogProps { - } - export declare const Dialog: ({ className, ...props }: DialogProps) => JSX.Element; -diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx -index 39b695f0bd98135afa1712daffed423a47133b30..3d8c9c3bf252b30235eb5b7a07a1a96c9240a80d 100644 ---- a/src/components/Dialog.tsx -+++ b/src/components/Dialog.tsx -@@ -1,12 +1,18 @@ --import React from 'react'; --import { Dialog as SaltDialog, type DialogProps as SaltDialogProps } from '@salt-ds/lab'; --import { themeClassName } from '@jpmorganchase/mosaic-theme'; --import classnames from 'clsx'; -+import React from "react"; -+import { -+ Dialog as SaltDialog, -+ type DialogProps as SaltDialogProps, -+} from "@salt-ds/core"; -+import { themeClassName } from "@jpmorganchase/mosaic-theme"; -+import classnames from "clsx"; - --import style from './Dialog.css'; -+import style from "./Dialog.css"; - - interface DialogProps extends SaltDialogProps {} - - export const Dialog = ({ className, ...props }: DialogProps) => ( -- -+ - ); -diff --git a/src/components/PersistEditDialog/index.tsx b/src/components/PersistEditDialog/index.tsx -index de12636b7101a004bf8affbc6afc186d1f95b07e..7087a92c659b53f548f8cc30c2ae8c0da80fb94b 100644 ---- a/src/components/PersistEditDialog/index.tsx -+++ b/src/components/PersistEditDialog/index.tsx -@@ -1,17 +1,18 @@ --import React, { FC, useState } from 'react'; --import md5 from 'md5'; --import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; --import { $convertToMarkdownString } from '@lexical/markdown'; --import { Link, P2, Button } from '@jpmorganchase/mosaic-components'; --import { ButtonBar, DialogTitle, DialogContent, DialogActions } from '@salt-ds/lab'; --import { SourceWorkflowMessageEvent } from '@jpmorganchase/mosaic-types'; -- --import { useEditorUser, usePageState } from '../../store'; --import transformers from '../../transformers'; --import { PersistStatus } from './PersistStatus'; --import { Dialog } from '../Dialog'; --import style from './index.css'; --import useWorkflowFeed from '../../hooks/useWorkflowFeed'; -+import React, { FC, useState } from "react"; -+import md5 from "md5"; -+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -+import { $convertToMarkdownString } from "@lexical/markdown"; -+import { Link, P2, Button } from "@jpmorganchase/mosaic-components"; -+import { ButtonBar } from "@salt-ds/lab"; -+import { DialogHeader, DialogContent, DialogActions } from "@salt-ds/core"; -+import { SourceWorkflowMessageEvent } from "@jpmorganchase/mosaic-types"; -+ -+import { useEditorUser, usePageState } from "../../store"; -+import transformers from "../../transformers"; -+import { PersistStatus } from "./PersistStatus"; -+import { Dialog } from "../Dialog"; -+import style from "./index.css"; -+import useWorkflowFeed from "../../hooks/useWorkflowFeed"; - - interface InfoProps { - isRaising: boolean; -@@ -23,13 +24,13 @@ const Info: FC = ({ isRaising, prHref, error }) => - !isRaising && !prHref && !error ? ( - <> - -- The content of this page resides in a Git repository and to update it requires a Pull -- Request which will be reviewed by the content owners. -+ The content of this page resides in a Git repository and to update it -+ requires a Pull Request which will be reviewed by the content owners. - -
- -- Should you decide to stop editing before creating the Pull Request then all changes will be -- lost. -+ Should you decide to stop editing before creating the Pull Request then -+ all changes will be lost. - - - ) : null; -@@ -48,13 +49,13 @@ export const PersistDialog = ({ meta, persistUrl }: PersistDialogProps) => { - const [error, setError] = useState(null); - const [progress, setProgress] = useState([]); - -- const open = pageState === 'SAVING'; -- const state = prHref !== null ? 'success' : 'info'; -+ const open = pageState === "SAVING"; -+ const state = prHref !== null ? "success" : "info"; - - const handleOpenChange = (newOpen: boolean) => { - setIsRaising(newOpen); - if (!newOpen) { -- setPageState('EDIT'); -+ setPageState("EDIT"); - setPrHref(null); - setProgress([]); - } -@@ -65,19 +66,21 @@ export const PersistDialog = ({ meta, persistUrl }: PersistDialogProps) => { - }; - - const handleErrorMessage = (errorMessage: string) => { -- setError(errorMessage ? errorMessage : 'Sorry - an unexpected error has occurred'); -+ setError( -+ errorMessage ? errorMessage : "Sorry - an unexpected error has occurred" -+ ); - setPrHref(null); - setProgress([]); - setIsRaising(false); - }; - -- const handleCompleteMessage = message => { -+ const handleCompleteMessage = (message) => { - setPrHref(message.message?.links?.self[0]?.href); - setIsRaising(false); - }; - -- const handleSuccessMessage = message => { -- setProgress(prevState => [...prevState, message]); -+ const handleSuccessMessage = (message) => { -+ setProgress((prevState) => [...prevState, message]); - }; - - const { sendWorkflowProgressMessage } = useWorkflowFeed( -@@ -103,14 +106,14 @@ export const PersistDialog = ({ meta, persistUrl }: PersistDialogProps) => { - user: { sid, name: displayName, email }, - route: meta.route, - markdown, -- name: 'save' -+ name: "save", - }), - md5(`${sid.toLowerCase()} - save`) - ); - } - }); - } catch (e) { -- setError('Sorry - an unexpected error has occurred'); -+ setError("Sorry - an unexpected error has occurred"); - setPrHref(null); - setIsRaising(false); - setProgress([]); -@@ -118,12 +121,19 @@ export const PersistDialog = ({ meta, persistUrl }: PersistDialogProps) => { - }; - - return ( -- -- -- {!prHref ? 'Save Changes' : 'Pull Request Created Successfully'} -- -+ -+ - -- {(isRaising || error) && !prHref && } -+ {(isRaising || error) && !prHref && ( -+ -+ )} - - {!isRaising && prHref && ( - -@@ -134,7 +144,7 @@ export const PersistDialog = ({ meta, persistUrl }: PersistDialogProps) => { - - - - @@ -257,6 +300,7 @@ describe("Given a ComboBox", () => {
); + cy.findByRole("combobox").should("be.disabled"); cy.realPress("Tab"); cy.findByRole("button", { name: "start" }).should("be.focused"); @@ -265,6 +309,39 @@ describe("Given a ComboBox", () => { cy.findByRole("button", { name: "end" }).should("be.focused"); }); + it("should not receive focus via mouse click if it is disabled", () => { + cy.mount(); + cy.findByRole("combobox").realClick(); + // Regression - #3369 + cy.get(".saltComboBox").should("not.have.class", "saltComboBox-focused"); + + cy.findByRole("combobox").should("be.disabled").should("not.be.focused"); + }); + + it("should not stay focus if disabled after option selection", () => { + // Regression - #3369 + const DisabledAfterSelection = () => { + const [disabled, setDisabled] = useState(false); + const handleSelectionChange = () => { + setDisabled(true); + }; + return ( + + ); + }; + + cy.mount(); + cy.realPress("Tab"); + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + + cy.get(".saltComboBox").should("not.have.class", "saltComboBox-focused"); + cy.findByRole("combobox").should("be.disabled").should("not.be.focused"); + }); + it("should not allow you to select a disabled option", () => { const selectionChangeSpy = cy.stub().as("selectionChange"); cy.mount(); @@ -281,6 +358,42 @@ describe("Given a ComboBox", () => { cy.get("@selectionChange").should("not.have.been.called"); }); + it("should not allow you to select a disabled option if selectOnTab is true", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount( + + ); + cy.findByRole("combobox").realClick(); + cy.findByRole("option", { name: "California" }).should( + "have.attr", + "aria-disabled", + "true" + ); + cy.realType("California"); + cy.realPress("Tab"); + cy.get("@selectionChange").should("not.have.been.called"); + }); + + it("should not allow you to select a option on Tab press if list is not open", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount( + + ); + cy.findByRole("combobox").realClick(); + cy.realType("alabama"); + cy.realPress("Tab"); + cy.get("@selectionChange").should("not.have.been.called"); + }); + it("should allow multiple options to be selected with a mouse", () => { const selectionChangeSpy = cy.stub().as("selectionChange"); cy.mount(); @@ -354,6 +467,49 @@ describe("Given a ComboBox", () => { cy.findByRole("combobox").should("have.value", ""); }); + it("should allow multiselect to select on tab key press if selectOnTab is passed as true", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + cy.findByRole("combobox").realClick(); + cy.realType("Ala"); + cy.realPress("Tab"); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alabama"]) + ); + cy.findByRole("button", { name: /^Alabama/ }).should("be.visible"); + }); + + it("by default multiselect should not allow to select on tab key press", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + cy.findByRole("combobox").realClick(); + cy.realType("Ala"); + cy.realPress("Tab"); + cy.get("@selectionChange").should("not.have.been.called"); + }); + + it("should not allow multiselect to deselect the already selected value on tab key press if selectOnTab is passed as true", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + cy.findByRole("combobox").realClick(); + cy.realType("Ala"); + cy.realPress("Tab"); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alabama"]) + ); + cy.findByRole("button", { name: /^Alabama/ }).should("be.visible"); + cy.findByRole("combobox").realClick(); + cy.realType("Alabama"); + cy.realPress("Tab"); + cy.findByRole("button", { name: /^Alabama/ }).should("be.visible"); + cy.findByRole("combobox").realClick(); + cy.findByRole("option", { name: "Alabama" }).should("be.ariaSelected"); + }); + it("should have form field support", () => { cy.mount(); cy.findByRole("combobox").should("have.accessibleName", "State"); @@ -375,11 +531,10 @@ describe("Given a ComboBox", () => { it("should allow showing an empty message when there are no options", () => { cy.mount(); cy.findByRole("combobox").realClick(); - cy.realType("Missing"); cy.findAllByRole("option").should("have.length", 1); cy.findByRole("option").should( "have.text", - `No results found for "Missing"` + `No results found for "Yelloww"` ); }); @@ -539,4 +694,32 @@ describe("Given a ComboBox", () => { cy.findByTestId(FLOATING_TEST_ID).should("exist"); }); + + it("should default to defaultValue when no defaultSelected is set", () => { + cy.mount( + + + ); + cy.findByRole("combobox").should("have.value", "Alaska"); + }); + it("should default to defaultValue when both defaultValue and defaultSelected are set", () => { + cy.mount( + + + ); + cy.findByRole("combobox").should("have.value", "Alaska"); + }); + it("should default to defaultSelected value when defaultValue is not set", () => { + cy.mount( + + + ); + cy.findByRole("combobox").should("have.value", "Alaska"); + }); }); diff --git a/packages/core/src/__tests__/__e2e__/divider/Divider.cy.tsx b/packages/core/src/__tests__/__e2e__/divider/Divider.cy.tsx new file mode 100644 index 0000000000..08214237ef --- /dev/null +++ b/packages/core/src/__tests__/__e2e__/divider/Divider.cy.tsx @@ -0,0 +1,28 @@ +import * as dividerStories from "@stories/divider/divider.stories"; +import { composeStories } from "@storybook/react"; +import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessibility"; + +const composedStories = composeStories(dividerStories); +const { Variants, Vertical } = composedStories; + +describe("GIVEN a Divider", () => { + checkAccessibility(composedStories); + + it('should have the role "separator" and aria-orientation horizontal', () => { + cy.mount(); + cy.findAllByRole("separator").should( + "have.attr", + "aria-orientation", + "horizontal" + ); + }); + + it("should have vertical aria-orientation when it has vertical orientation", () => { + cy.mount(); + cy.findAllByRole("separator").should( + "have.attr", + "aria-orientation", + "vertical" + ); + }); +}); diff --git a/packages/core/src/__tests__/__e2e__/dropdown/Dropdown.cy.tsx b/packages/core/src/__tests__/__e2e__/dropdown/Dropdown.cy.tsx index b4af110136..e042a1b269 100644 --- a/packages/core/src/__tests__/__e2e__/dropdown/Dropdown.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/dropdown/Dropdown.cy.tsx @@ -3,6 +3,7 @@ import * as dropdownStories from "@stories/dropdown/dropdown.stories"; import { Dropdown } from "@salt-ds/core"; import { CustomFloatingComponentProvider, FLOATING_TEST_ID } from "../common"; +import { useState } from "react"; const { Default, @@ -16,6 +17,7 @@ const { CustomValue, WithDefaultSelected, ObjectValue, + LongList, } = composeStories(dropdownStories); describe("Given a Dropdown", () => { @@ -174,7 +176,7 @@ describe("Given a Dropdown", () => { }); it("should support keyboard navigation", () => { - cy.mount(); + cy.mount(); cy.findByRole("combobox").realClick(); cy.findByRole("listbox").should("exist"); @@ -187,22 +189,22 @@ describe("Given a Dropdown", () => { cy.realPress(["ArrowDown"]); cy.findAllByRole("option").eq(1).should("be.activeDescendant"); - // should try to go down 10, but only 9 items in list + // should try to go down by the number of visible items in list cy.realPress(["PageDown"]); - cy.findAllByRole("option").eq(-1).should("be.activeDescendant"); - - // should not wrap - cy.realPress(["ArrowDown"]); - cy.findAllByRole("option").eq(-1).should("be.activeDescendant"); + cy.findAllByRole("option").eq(14).should("be.activeDescendant"); - // should try to go up 10, but only 9 items in list + // should try to go up by the number of visible items in list cy.realPress(["PageUp"]); - cy.findAllByRole("option").eq(0).should("be.activeDescendant"); + cy.findAllByRole("option").eq(1).should("be.activeDescendant"); // should go to the last item cy.realPress(["End"]); cy.findAllByRole("option").eq(-1).should("be.activeDescendant"); + // should not wrap + cy.realPress(["ArrowDown"]); + cy.findAllByRole("option").eq(-1).should("be.activeDescendant"); + cy.realPress(["ArrowUp"]); cy.findAllByRole("option").eq(-2).should("be.activeDescendant"); @@ -224,7 +226,7 @@ describe("Given a Dropdown", () => { cy.findByRole("combobox").should("have.text", "California"); }); - it("should not receive focus if it is disabled", () => { + it("should not receive focus via tab if it is disabled", () => { cy.mount(
@@ -240,6 +242,36 @@ describe("Given a Dropdown", () => { cy.findByRole("button", { name: "end" }).should("be.focused"); }); + it("should not receive focus via mouse click if it is disabled", () => { + cy.mount(); + cy.findByRole("combobox").realClick(); + + cy.findByRole("combobox").should("be.disabled").should("not.be.focused"); + cy.findByRole("listbox").should("not.exist"); + }); + + it("should not stay focus if disabled after option selection", () => { + // Regression - #3369 + const DisabledAfterSelection = () => { + const [disabled, setDisabled] = useState(false); + const handleSelectionChange = () => { + setDisabled(true); + }; + return ( + + ); + }; + cy.mount(); + cy.realPress("Tab"); + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + + cy.findByRole("combobox").should("be.disabled").should("not.be.focused"); + }); + it("should not allow you to select a disabled option", () => { const selectionChangeSpy = cy.stub().as("selectionChange"); cy.mount(); @@ -250,6 +282,9 @@ describe("Given a Dropdown", () => { "true" ); cy.realType("California"); + cy.findByRole("option", { name: "California" }).should( + "be.activeDescendant" + ); cy.realPress("Enter"); cy.get("@selectionChange").should("not.have.been.called"); cy.findByRole("option", { name: "California" }).realClick(); @@ -400,6 +435,27 @@ describe("Given a Dropdown", () => { cy.findByRole("combobox").should("have.text", "Placeholder"); }); + it("should support typeahead", () => { + cy.mount(); + cy.realPress("Tab"); + cy.realType("A"); + cy.findByRole("listbox").should("exist"); + cy.findByRole("option", { name: "Alabama" }).should("be.activeDescendant"); + + cy.realType("A"); + cy.findByRole("option", { name: "Alaska" }).should("be.activeDescendant"); + + cy.realType("A"); + cy.findByRole("option", { name: "Arizona" }).should("be.activeDescendant"); + + cy.wait(500); + + cy.realType("Co"); + cy.findByRole("option", { name: "Connecticut" }).should( + "be.activeDescendant" + ); + }); + it("should render the custom floating component", () => { cy.mount( diff --git a/packages/core/src/__tests__/__e2e__/file-drop-zone/FileDropZone.cy.tsx b/packages/core/src/__tests__/__e2e__/file-drop-zone/FileDropZone.cy.tsx index 5540a94f73..7d1ff50ffa 100644 --- a/packages/core/src/__tests__/__e2e__/file-drop-zone/FileDropZone.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/file-drop-zone/FileDropZone.cy.tsx @@ -53,6 +53,23 @@ describe("Given a file drop zone", () => { "saltFileDropZone-success" ); }); + it("should allow selecting the same file from the button after reset", () => { + const file = { + contents: Cypress.Buffer.from("image1"), + fileName: "image1", + mimeType: "image/jpg", + lastModified: Date.now(), + }; + const changeSpy = cy.stub().as("changeSpy"); + cy.mount(); + cy.get("input[type=file]").selectFile(file, { force: true }); + cy.get("@changeSpy").should("have.been.calledOnce"); + cy.findByRole("button", { name: "Reset" }).realClick(); + cy.get("input[type=file]").selectFile(file, { force: true }); + // Note: this could be a false positive, test will pass regardless whether the fix is there + // have the test here to note the behaviour we want, #3591 + cy.get("@changeSpy").should("have.been.calledTwice"); + }); it("should trigger onDrop when files are dropped", () => { const dropSpy = cy.stub().as("dropSpy"); cy.mount(); diff --git a/packages/core/src/__tests__/__e2e__/list-box/ListBox.cy.tsx b/packages/core/src/__tests__/__e2e__/list-box/ListBox.cy.tsx new file mode 100644 index 0000000000..eb094ee521 --- /dev/null +++ b/packages/core/src/__tests__/__e2e__/list-box/ListBox.cy.tsx @@ -0,0 +1,219 @@ +import { composeStories } from "@storybook/react"; +import * as listBoxStories from "@stories/list-box/list-box.stories"; + +const { + SingleSelect, + Multiselect, + Disabled, + DisabledOption, + DefaultSelectedSingleSelect, + DefaultSelectedMultiselect, + Grouped, + Scrolling, +} = composeStories(listBoxStories); + +describe("GIVEN a List box", () => { + it("should allow selection with a mouse", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + + cy.findByRole("option", { name: "Alaska" }).realHover(); + cy.findByRole("option", { name: "Alaska" }).realClick(); + cy.findByRole("option", { name: "Alaska" }).should("be.activeDescendant"); + + cy.findByRole("listbox").should("be.focused"); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alaska"]) + ); + }); + + it("should allow selection with a keyboard", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + + cy.realPress("Tab"); + cy.findByRole("option", { name: "Alabama" }).should("be.activeDescendant"); + cy.realPress("ArrowDown"); + cy.findByRole("option", { name: "Alaska" }).should("be.activeDescendant"); + cy.realPress("Enter"); + + cy.findByRole("listbox").should("be.focused"); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alaska"]) + ); + }); + + it("should focus the selected item when the list is focused in single select", () => { + cy.mount(); + cy.realPress("Tab"); + cy.findByRole("option", { name: "Arkansas" }).should("be.activeDescendant"); + }); + + it("should focus the selected item when the list is focused in multiselect", () => { + cy.mount(); + cy.realPress("Tab"); + cy.findByRole("option", { name: "Arkansas" }).should("be.activeDescendant"); + }); + + it("should support keyboard navigation", () => { + cy.mount(); + + cy.realPress(["Tab"]); + cy.findAllByRole("option").eq(0).should("be.activeDescendant"); + + // should not wrap + cy.realPress(["ArrowUp"]); + cy.findAllByRole("option").eq(0).should("be.activeDescendant"); + + cy.realPress(["ArrowDown"]); + cy.findAllByRole("option").eq(1).should("be.activeDescendant"); + + // should try to go down by the number of visible items in list + cy.realPress(["PageDown"]); + cy.findAllByRole("option").eq(8).should("be.activeDescendant"); + + // should try to go up by the number of visible items in list + cy.realPress(["PageUp"]); + cy.findAllByRole("option").eq(1).should("be.activeDescendant"); + + // should go to the last item + cy.realPress(["End"]); + cy.findAllByRole("option").eq(-1).should("be.activeDescendant"); + + // should not wrap + cy.realPress(["ArrowDown"]); + cy.findAllByRole("option").eq(-1).should("be.activeDescendant"); + + cy.realPress(["ArrowUp"]); + cy.findAllByRole("option").eq(-2).should("be.activeDescendant"); + + // should go to the first item + cy.realPress(["Home"]); + cy.findAllByRole("option").eq(0).should("be.activeDescendant"); + }); + + it("should not receive focus if it is disabled", () => { + cy.mount( +
+ + + +
+ ); + cy.findByRole("listbox").should("have.attr", "aria-disabled", "true"); + cy.realPress("Tab"); + cy.findByRole("button", { name: "start" }).should("be.focused"); + cy.realPress("Tab"); + cy.findByRole("listbox").should("not.be.focused"); + cy.findByRole("button", { name: "end" }).should("be.focused"); + }); + + it("should not allow you to select a disabled option", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + cy.findByRole("option", { name: "Arizona" }).should( + "have.attr", + "aria-disabled", + "true" + ); + cy.realPress("Tab"); + cy.realPress("ArrowDown"); + cy.realPress("ArrowDown"); + cy.findByRole("option", { name: "Arizona" }).should("be.activeDescendant"); + cy.findByRole("option", { name: "Arizona" }).realPress("Enter"); + cy.get("@selectionChange").should("not.have.been.called"); + cy.findByRole("option", { name: "Arizona" }).realClick(); + cy.get("@selectionChange").should("not.have.been.called"); + }); + + it("should allow multiple options to be selected with a mouse", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + + cy.findByRole("listbox").should( + "have.attr", + "aria-multiselectable", + "true" + ); + + cy.findByRole("option", { name: "Alabama" }).realClick(); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alabama"]) + ); + cy.findByRole("option", { name: "Alabama" }).should( + "have.attr", + "aria-selected", + "true" + ); + cy.findByRole("option", { name: "Alaska" }).realClick(); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alabama", "Alaska"]) + ); + cy.findByRole("option", { name: "Alaska" }).should( + "have.attr", + "aria-selected", + "true" + ); + }); + + it("should allow multiple options to be selected with the keyboard", () => { + const selectionChangeSpy = cy.stub().as("selectionChange"); + cy.mount(); + cy.realPress("Tab"); + cy.realPress("Enter"); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alabama"]) + ); + cy.findByRole("option", { name: "Alabama" }).should( + "have.attr", + "aria-selected", + "true" + ); + cy.realPress("ArrowDown"); + cy.realPress("Enter"); + cy.get("@selectionChange").should( + "have.been.calledWith", + Cypress.sinon.match.any, + Cypress.sinon.match.array.deepEquals(["Alabama", "Alaska"]) + ); + cy.findByRole("option", { name: "Alaska" }).should( + "have.attr", + "aria-selected", + "true" + ); + }); + + it("should support grouping", () => { + cy.mount(); + cy.findByRole("group", { name: "A" }).should("exist"); + cy.findByRole("group", { name: "A" }) + .findByRole("option", { name: "Alabama" }) + .should("exist"); + }); + + it("should support typeahead", () => { + cy.mount(); + cy.realPress("Tab"); + cy.realType("A"); + cy.findByRole("listbox").should("exist"); + cy.findByRole("option", { name: "Alaska" }).should("be.activeDescendant"); + + cy.realType("A"); + cy.findByRole("option", { name: "Arizona" }).should("be.activeDescendant"); + + cy.wait(500); + + cy.realType("Alas"); + cy.findByRole("option", { name: "Alaska" }).should("be.activeDescendant"); + }); +}); diff --git a/packages/core/src/__tests__/__e2e__/menu/Menu.cy.tsx b/packages/core/src/__tests__/__e2e__/menu/Menu.cy.tsx index 9cab3082ed..ae6ab38eec 100644 --- a/packages/core/src/__tests__/__e2e__/menu/Menu.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/menu/Menu.cy.tsx @@ -12,6 +12,9 @@ describe("Given a Menu", () => { cy.findByRole("menu").should("not.exist"); cy.findByRole("button", { name: "Open Menu" }).realClick(); cy.findByRole("menu").should("exist"); + // Regression - #3636 + cy.get(".saltMenuPanel").should("have.css", "z-index", "1500"); + cy.get("@openChangeSpy").should("have.been.calledWith", true); cy.findByRole("menuitem", { name: "Copy" }).realClick(); cy.on("window:alert", (str) => { diff --git a/packages/core/src/__tests__/__e2e__/salt-provider/SaltProvider.cy.tsx b/packages/core/src/__tests__/__e2e__/salt-provider/SaltProvider.cy.tsx index 930c843b89..57bd87b7af 100644 --- a/packages/core/src/__tests__/__e2e__/salt-provider/SaltProvider.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/salt-provider/SaltProvider.cy.tsx @@ -19,8 +19,15 @@ const TestComponent = ({ className?: string; }) => { const density = useDensity(); - const { theme, mode, UNSTABLE_corner, UNSTABLE_accent, themeNext } = - useTheme(); + const { + theme, + mode, + UNSTABLE_corner, + UNSTABLE_accent, + themeNext, + UNSTABLE_actionFont, + UNSTABLE_headingFont, + } = useTheme(); const { announce } = useAriaAnnouncer(); const announcerPresent = typeof announce === "function"; @@ -34,6 +41,8 @@ const TestComponent = ({ data-announcer={announcerPresent} data-corner={UNSTABLE_corner} data-accent={UNSTABLE_accent} + data-heading-font={UNSTABLE_headingFont} + data-action-font={UNSTABLE_actionFont} data-themeNext={themeNext} /> ); @@ -82,7 +91,7 @@ describe("Given a SaltProvider", () => { }); describe("with props set", () => { - it("should apply correct default value for Density and add an AriaAnnouncer", () => { + it("should apply correct default value for density and add an AriaAnnouncer", () => { mount( @@ -95,7 +104,7 @@ describe("Given a SaltProvider", () => { .and("have.attr", "data-announcer", "true"); }); - it("should apply correct default value for Theme and add an AriaAnnouncer", () => { + it("should apply correct default value for mode and add an AriaAnnouncer", () => { mount( @@ -110,14 +119,40 @@ describe("Given a SaltProvider", () => { it("should apply values specified in props", () => { mount( - + + + + ); + cy.get("#test-1") + .should("exist") + .and("have.attr", "data-density", "high") + .and("have.attr", "data-mode", "dark") + .and("have.attr", "data-theme", "custom-theme") + .and("have.attr", "data-announcer", "true"); + }); + + it("should allow pass in multiple theme names", () => { + mount( + ); + + cy.get("html") + .should("exist") + .and("have.attr", "data-mode", "dark") + .and("have.class", "custom-theme-1 custom-theme-2") + .and("have.class", "salt-density-high"); + cy.get("#test-1") .should("exist") .and("have.attr", "data-density", "high") .and("have.attr", "data-mode", "dark") + .and("have.attr", "data-theme", "custom-theme-1 custom-theme-2") .and("have.attr", "data-announcer", "true"); }); }); @@ -180,7 +215,12 @@ describe("Given a SaltProvider", () => { describe("when root is passed to applyClassesTo", () => { it("should apply the given theme and density class names to the html element", () => { mount( - + ); @@ -190,6 +230,7 @@ describe("Given a SaltProvider", () => { cy.get("html") .should("exist") .and("have.attr", "data-mode", "dark") + .and("have.class", "custom-theme") .and("have.class", "salt-density-high"); }); }); @@ -197,7 +238,12 @@ describe("Given a SaltProvider", () => { describe("when scope is passed to applyClassesTo", () => { it("should create div element with correct classes applied even if it is the root level provider", () => { mount( - + ); @@ -205,6 +251,7 @@ describe("Given a SaltProvider", () => { cy.get("div.salt-provider") .should("have.length", 1) .and("have.attr", "data-mode", "dark") + .and("have.class", "custom-theme") .and("have.class", "salt-density-high"); }); }); @@ -277,6 +324,9 @@ describe("Given a SaltProviderNext", () => { .and("have.attr", "data-mode", "light") .and("have.attr", "data-corner", "sharp") .and("have.attr", "data-accent", "blue") + .and("have.attr", "data-heading-font", "Open Sans") + .and("have.attr", "data-action-font", "Open Sans") + .and("have.class", "salt-theme") .and("have.class", "salt-theme-next") .and("have.class", "salt-density-medium"); }); @@ -293,11 +343,49 @@ describe("Given a SaltProviderNext", () => { .and("have.attr", "data-announcer", "true") .and("have.attr", "data-corner", "sharp") .and("have.attr", "data-accent", "blue") + .and("have.attr", "data-heading-font", "Open Sans") + .and("have.attr", "data-action-font", "Open Sans") .and("have.attr", "data-themeNext", "true"); cy.get("[aria-live]").should("exist"); }); }); + describe("with props set", () => { + it("should allow pass in multiple theme names", () => { + mount( + + + + ); + + cy.get("html") + .should("exist") + .and("have.attr", "data-mode", "dark") + .and("have.attr", "data-accent", "teal") + .and("have.attr", "data-corner", "rounded") + .and("have.class", "salt-theme") + .and("have.class", "salt-theme-next") + .and("have.class", "custom-theme-1") + .and("have.class", "custom-theme-2") + .and("have.class", "salt-density-high"); + + cy.get("#test-1") + .should("exist") + .and("have.attr", "data-density", "high") + .and("have.attr", "data-mode", "dark") + .and("have.attr", "data-accent", "teal") + .and("have.attr", "data-corner", "rounded") + .and("have.attr", "data-theme", "custom-theme-1 custom-theme-2") + .and("have.attr", "data-announcer", "true"); + }); + }); + describe("when nested", () => { it("should inherit values not passed as props", () => { mount( @@ -306,6 +394,8 @@ describe("Given a SaltProviderNext", () => { mode="dark" corner="rounded" accent="teal" + headingFont="Amplitude" + actionFont="Amplitude" > @@ -323,6 +413,8 @@ describe("Given a SaltProviderNext", () => { .and("have.attr", "data-mode", "dark") .and("have.attr", "data-corner", "rounded") .and("have.attr", "data-accent", "teal") + .and("have.attr", "data-heading-font", "Amplitude") + .and("have.attr", "data-action-font", "Amplitude") .and("have.attr", "data-announcer", "true"); cy.get("#test-2") @@ -331,6 +423,8 @@ describe("Given a SaltProviderNext", () => { .and("have.attr", "data-mode", "dark") .and("have.attr", "data-corner", "rounded") .and("have.attr", "data-accent", "teal") + .and("have.attr", "data-heading-font", "Amplitude") + .and("have.attr", "data-action-font", "Amplitude") .and("have.attr", "data-announcer", "true"); }); it("should take different values set as props", () => { @@ -340,12 +434,16 @@ describe("Given a SaltProviderNext", () => { mode="dark" corner="rounded" accent="teal" + headingFont="Amplitude" + actionFont="Amplitude" > @@ -361,6 +459,8 @@ describe("Given a SaltProviderNext", () => { .and("have.attr", "data-mode", "dark") .and("have.attr", "data-corner", "rounded") .and("have.attr", "data-accent", "teal") + .and("have.attr", "data-heading-font", "Amplitude") + .and("have.attr", "data-action-font", "Amplitude") .and("have.attr", "data-announcer", "true"); cy.get("#test-2") @@ -369,6 +469,8 @@ describe("Given a SaltProviderNext", () => { .and("have.attr", "data-mode", "dark") .and("have.attr", "data-corner", "sharp") .and("have.attr", "data-accent", "blue") + .and("have.attr", "data-heading-font", "Open Sans") + .and("have.attr", "data-action-font", "Open Sans") .and("have.attr", "data-announcer", "true"); }); }); diff --git a/packages/core/src/__tests__/__e2e__/switch/Switch.cy.tsx b/packages/core/src/__tests__/__e2e__/switch/Switch.cy.tsx index 4b73dd80e3..e43e863312 100644 --- a/packages/core/src/__tests__/__e2e__/switch/Switch.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/switch/Switch.cy.tsx @@ -149,4 +149,11 @@ describe("GIVEN a Switch", () => { }); }); }); + + describe("WHEN used without label", () => { + it("THEN should NOT render label span", () => { + cy.mount(); + cy.get(".saltSwitch-label").should("not.exist"); + }); + }); }); diff --git a/packages/core/src/__tests__/__e2e__/text/Text.cy.tsx b/packages/core/src/__tests__/__e2e__/text/Text.cy.tsx index 1a317b9141..e7a73e5ee5 100644 --- a/packages/core/src/__tests__/__e2e__/text/Text.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/text/Text.cy.tsx @@ -1,8 +1,10 @@ +import { VALIDATION_NAMED_STATUS } from "../../../status-indicator"; import { Text, Display1, Display2, Display3, + Display4, H1, H2, H3, @@ -34,6 +36,7 @@ const componentsArray = [ { component: Display1, name: "Display1", tag: "span" }, { component: Display2, name: "Display2", tag: "span" }, { component: Display3, name: "Display3", tag: "span" }, + { component: Display4, name: "Display4", tag: "span" }, { component: H1, name: "H1", tag: "h1" }, { component: H2, name: "H2", tag: "h2" }, { component: H3, name: "H3", tag: "h3" }, @@ -52,6 +55,16 @@ describe("GIVEN a Text Component", () => { cy.mount({textExample}); cy.get(tag).should("have.class", "saltText"); }); + + it(`${name} component should return custom className passed in`, () => { + const customClass = "customClass"; + const Component = component; + + cy.mount({textExample}); + cy.get(tag) + .should("have.class", "saltText") + .should("have.class", customClass); + }); }); }); @@ -81,27 +94,38 @@ describe("GIVEN a Text component with maxRows=2 ", () => { }); }); -// Variant -describe("GIVEN a Text component with variant=primary ", () => { - componentsArray.forEach(({ component, name }) => { - it(`${name} should have class saltText-primary`, () => { - const Component = component; +// Variant - deprecated prop, keep until we remove +const VARIANTS = ["primary", "secondary"] as const; +VARIANTS.forEach((variant) => { + describe(`GIVEN a Text component with variant=${variant} `, () => { + componentsArray.forEach(({ component, name }) => { + it(`${name} should have class saltText-${variant}`, () => { + const Component = component; - cy.mount({textExample}); - cy.get(".saltText").should("have.class", "saltText-primary"); + cy.mount({textExample}); + cy.get(".saltText").should("have.class", `saltText-${variant}`); + }); }); }); }); -describe("GIVEN a Text component with variant=secondary ", () => { - componentsArray.forEach(({ component, name }) => { - it(`${name} should have class saltText-secondary`, () => { - const Component = component; - cy.mount({textExample}); - cy.get(".saltText").should("have.class", "saltText-secondary"); +const COLORS = ["primary", "secondary", ...VALIDATION_NAMED_STATUS] as const; +COLORS.forEach((color) => { + describe(`GIVEN a Text component with color=${color} `, () => { + componentsArray.forEach(({ component, name }) => { + it(`${name} should have class saltText-${color}`, () => { + const Component = component; + + cy.mount({textExample}); + cy.get(".saltText").should("have.class", `saltText-${color}`); + }); }); }); }); +it(`GIVEN a Text component with color="inherit", it should NOT have color class `, () => { + cy.mount({textExample}); + cy.get(".saltText").should("not.have.class", `saltText-inherit`); +}); // styleAs describe("GIVEN Text component with styleAs=h1", () => { diff --git a/packages/core/src/__tests__/__e2e__/toast/Toast.cy.tsx b/packages/core/src/__tests__/__e2e__/toast/Toast.cy.tsx index 21c544d1c7..076f202272 100644 --- a/packages/core/src/__tests__/__e2e__/toast/Toast.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/toast/Toast.cy.tsx @@ -2,6 +2,7 @@ import { composeStories } from "@storybook/react"; import { Toast, ToastContent } from "@salt-ds/core"; import * as toastStories from "@stories/toast/toast.stories"; import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessibility"; +import { LinkedIcon } from "@salt-ds/icons"; const composedStories = composeStories(toastStories); @@ -52,4 +53,13 @@ describe("Given a Toast", () => { ); cy.findByRole("img", { name: "success" }).should("exist"); }); + + it("AND custom icon, THEN renders with custom icon", () => { + cy.mount( + } status={"success"}> + Toast content + + ); + cy.findAllByTestId("LinkedIcon").should("exist"); + }); }); diff --git a/packages/core/src/accordion/Accordion.tsx b/packages/core/src/accordion/Accordion.tsx index 0f51cc8251..f852e23898 100644 --- a/packages/core/src/accordion/Accordion.tsx +++ b/packages/core/src/accordion/Accordion.tsx @@ -18,6 +18,10 @@ export interface AccordionProps extends ComponentPropsWithoutRef<"div"> { * Whether the accordion is expanded by default. */ defaultExpanded?: boolean; + /** + * Side to align the Accordion's indicator. Defaults to `left`. + */ + indicatorSide?: "left" | "right"; /** * Callback fired when the accordion is toggled. */ @@ -41,6 +45,7 @@ export const Accordion = forwardRef( defaultExpanded, expanded: expandedProp, disabled, + indicatorSide = "left", id: idProp, onToggle, status, @@ -74,6 +79,7 @@ export const Accordion = forwardRef( value, toggle, expanded, + indicatorSide, disabled: Boolean(disabled), id: id ?? "", status, diff --git a/packages/core/src/accordion/AccordionContext.ts b/packages/core/src/accordion/AccordionContext.ts index a6be9a372f..7ec22260fe 100644 --- a/packages/core/src/accordion/AccordionContext.ts +++ b/packages/core/src/accordion/AccordionContext.ts @@ -6,6 +6,7 @@ export interface AccordionContextValue { expanded: boolean; toggle: (event: SyntheticEvent) => void; disabled: boolean; + indicatorSide: "left" | "right"; id: string; status?: "error" | "warning" | "success"; } @@ -17,6 +18,7 @@ export const AccordionContext = createContext( expanded: false, toggle: () => undefined, disabled: false, + indicatorSide: "left", id: "", } ); diff --git a/packages/core/src/accordion/AccordionHeader.css b/packages/core/src/accordion/AccordionHeader.css index 7be6abec13..5129a501cf 100644 --- a/packages/core/src/accordion/AccordionHeader.css +++ b/packages/core/src/accordion/AccordionHeader.css @@ -33,13 +33,8 @@ box-sizing: border-box; } -.saltAccordionHeader-icon { +.saltAccordionHeader .saltAccordionHeader-icon { height: var(--salt-size-base); - transition: transform var(--salt-duration-perceptible) ease-in-out; -} - -.saltAccordionHeader[aria-expanded="true"] > .saltAccordionHeader-icon { - transform: rotate(90deg); } .saltAccordionHeader-error { diff --git a/packages/core/src/accordion/AccordionHeader.tsx b/packages/core/src/accordion/AccordionHeader.tsx index 920531af07..51f9b83219 100644 --- a/packages/core/src/accordion/AccordionHeader.tsx +++ b/packages/core/src/accordion/AccordionHeader.tsx @@ -6,7 +6,7 @@ import { } from "react"; import { clsx } from "clsx"; import { StatusIndicator } from "../status-indicator"; -import { ChevronRightIcon } from "@salt-ds/icons"; +import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons"; import { useWindow } from "@salt-ds/window"; import { useComponentCssInjection } from "@salt-ds/styles"; @@ -25,12 +25,21 @@ export interface AccordionHeaderProps const withBaseName = makePrefixer("saltAccordionHeader"); +function ExpansionIcon({ expanded }: { expanded: boolean }) { + if (expanded) { + return ; + } + + return ; +} + export const AccordionHeader = forwardRef< HTMLButtonElement, AccordionHeaderProps >(function AccordionHeader(props, ref) { const { children, className, onClick, ...rest } = props; - const { value, expanded, toggle, disabled, id, status } = useAccordion(); + const { value, expanded, toggle, indicatorSide, disabled, id, status } = + useAccordion(); const targetWindow = useWindow(); useComponentCssInjection({ @@ -61,14 +70,15 @@ export const AccordionHeader = forwardRef< type="button" {...rest} > -