From df7f14c56f18b67328b44a33124c73b5d8172502 Mon Sep 17 00:00:00 2001 From: m-akinc <7282195+m-akinc@users.noreply.github.com> Date: Wed, 27 Mar 2024 18:52:38 -0500 Subject: [PATCH] Add interaction stories for buttons, number field, and anchor (#1919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐Ÿคจ Rationale Part of #495 ## ๐Ÿ‘ฉโ€๐Ÿ’ป Implementation I had to make some changes in `storybook-addon-pseudo-states` to support our styling, but now we can pull in that fixed release and add some initial visual tests. The way the addon typically works is that you set story parameters to select which component(s) should have the pseudo-state styling, e.g. ```js ButtonStory.parameters = { pseudo: { hover: ["#one", "#two", "#three"], focus: true, // means apply to all components active: ".some-class", }, } ``` However, this does work well with our matrix story approach. The effect of these parameters is just to set certain classes on the indicated elements. So, instead, we can bypass the story parameters and directly apply the classes to our elements. For example, any element with the class `hover` or `pseudo-hover-all` will be styled by the addon as if hovered. NOTE: Because we have to put the classes directly on the elements in our Story html, we can't put them on any shadow DOM elements, e.g. the inc/dec buttons of the number field. However, puting `pseudo--all` on a host element will cause all elements in its shadow DOM to be styled as if they have that state. This can create some visuals that are practically impossible (e.g. a number field and both of its inc/dec buttons all having keyboard focus at the same time), but they are still useful from a visual testing standpoint. The pseudo-states supported by the addon are: - `:hover` - `:active` - `:focus-visible` - `:focus-within` - `:focus` - `:visited` - `:link` - `:target` It is only interesting to test states that we specifically style for, so generally, that is hover, active, and some form of focused. Because our components use both `:focus-visible` (`focusVisible` from FAST) and `:focus-within`, I'm not distinguishing the two, and I'm setting classes for both whenever we want to see "focused" styling. We could potentially test all combinations of these pseudo-states, but that seems unnecessary, especially considering that we want to test these states in _conjunction_ with other appearance/config variations. So I'm limiting combination testing to just hovered+active, since that is a common use case for mouse-based interaction. To summarize, that means I'm proposing we test: - hover (only) - active (only) - focused (only) - hovered and active Initially, I'm adding new test matrix stories for: - anchor - anchor-button - button - menu-button - number-field - toggle-button These seemed to be some of the more interesting components in terms of having distinct keyboard-focused and/or active states, (in addition to hover, which applies to most/all? components). Some combinations of interaction states with control attribute values aren't interesting, e.g. a disabled control with focus or active interaction state. In order to exclude certain combinations, we had to refactor the `createMatrix` utility function and introduce a new `createMatrixInteractionsFromStates` function. ## ๐Ÿงช Testing Storybook visual tests ## โœ… Checklist - [x] I have updated the project documentation to reflect my changes or determined no changes are needed. --------- Co-authored-by: rajsite --- ...-ceebd404-0fb3-43e5-8f98-04f4eed64531.json | 7 + package-lock.json | 24 ++ packages/nimble-components/.storybook/main.js | 3 +- .../nimble-components/.storybook/manager.js | 5 +- packages/nimble-components/package.json | 1 + .../tests/anchor-button-matrix.stories.ts | 36 ++- .../src/anchor/tests/anchor-matrix.stories.ts | 40 ++- .../src/button/tests/button-matrix.stories.ts | 36 ++- .../tests/menu-button-matrix.stories.ts | 56 +++- .../tests/number-field-matrix.stories.ts | 37 ++- .../src/patterns/button/tests/states.ts | 1 + .../tests/toggle-button-matrix.stories.ts | 38 ++- .../src/utilities/tests/matrix.ts | 255 ++++++------------ .../src/utilities/tests/states.ts | 4 + 14 files changed, 330 insertions(+), 213 deletions(-) create mode 100644 change/@ni-nimble-components-ceebd404-0fb3-43e5-8f98-04f4eed64531.json diff --git a/change/@ni-nimble-components-ceebd404-0fb3-43e5-8f98-04f4eed64531.json b/change/@ni-nimble-components-ceebd404-0fb3-43e5-8f98-04f4eed64531.json new file mode 100644 index 0000000000..7a92885914 --- /dev/null +++ b/change/@ni-nimble-components-ceebd404-0fb3-43e5-8f98-04f4eed64531.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Add interaction stories for buttons, number field, and anchor", + "packageName": "@ni/nimble-components", + "email": "7282195+m-akinc@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/package-lock.json b/package-lock.json index 4f4fd874ca..b7d1560a0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29445,6 +29445,29 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/storybook-addon-pseudo-states": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-2.2.1.tgz", + "integrity": "sha512-4LoaiML0BM9sZcQbXjDhRh9jUUKIRTWEQMl91ihP2wIE10n+rL/5c8IBpNiMZLV1rnm24degEncSMY9ck+bpgg==", + "dev": true, + "peerDependencies": { + "@storybook/components": "^7.4.6", + "@storybook/core-events": "^7.4.6", + "@storybook/manager-api": "^7.4.6", + "@storybook/preview-api": "^7.4.6", + "@storybook/theming": "^7.4.6", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", @@ -33287,6 +33310,7 @@ "rollup-plugin-sourcemaps": "^0.6.3", "source-map-loader": "^5.0.0", "storybook": "^8.0.4", + "storybook-addon-pseudo-states": "^2.2.1", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.2.5", "typescript": "~4.9.5", diff --git a/packages/nimble-components/.storybook/main.js b/packages/nimble-components/.storybook/main.js index 59cdfe2581..dda6dc10f8 100644 --- a/packages/nimble-components/.storybook/main.js +++ b/packages/nimble-components/.storybook/main.js @@ -27,7 +27,8 @@ export const addons = [ getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('@storybook/addon-interactions'), getAbsolutePath('@chromatic-com/storybook'), - getAbsolutePath('@storybook/addon-webpack5-compiler-swc') + getAbsolutePath('@storybook/addon-webpack5-compiler-swc'), + getAbsolutePath('storybook-addon-pseudo-states') ]; export function webpackFinal(config) { config.module.rules.push({ diff --git a/packages/nimble-components/.storybook/manager.js b/packages/nimble-components/.storybook/manager.js index 1159c9cc0c..4a9b2df3af 100644 --- a/packages/nimble-components/.storybook/manager.js +++ b/packages/nimble-components/.storybook/manager.js @@ -15,5 +15,8 @@ addons.setConfig({ } } }, - theme + theme, + toolbar: { + 'storybook/pseudo-states/tool': { hidden: true } + } }); diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index 53af8b72ba..35f970487d 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -162,6 +162,7 @@ "rollup-plugin-sourcemaps": "^0.6.3", "source-map-loader": "^5.0.0", "storybook": "^8.0.4", + "storybook-addon-pseudo-states": "^2.2.1", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.2.5", "typescript": "~4.9.5", diff --git a/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts b/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts index 5306713f61..e2cdb65459 100644 --- a/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts +++ b/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts @@ -3,9 +3,15 @@ import { html, ViewTemplate, when } from '@microsoft/fast-element'; import { createMatrix, sharedMatrixParameters, - createMatrixThemeStory + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates } from '../../utilities/tests/matrix'; -import { disabledStates, DisabledState } from '../../utilities/tests/states'; +import { + disabledStates, + DisabledState, + disabledStateIsEnabled +} from '../../utilities/tests/states'; import { createStory } from '../../utilities/tests/storybook'; import { hiddenWrapper } from '../../utilities/tests/hidden'; import { textCustomizationWrapper } from '../../utilities/tests/text-customization'; @@ -18,7 +24,8 @@ import { type AppearanceVariantState, type PartVisibilityState, appearanceVariantStates, - partVisibilityStates + partVisibilityStates, + partVisibilityStatesOnlyLabel } from '../../patterns/button/tests/states'; const metadata: Meta = { @@ -59,6 +66,29 @@ export const anchorButtonThemeMatrix: StoryFn = createMatrixThemeStory( ]) ); +const interactionStatesHover = cartesianProduct([ + disabledStates, + appearanceStates, + appearanceVariantStates, + [partVisibilityStatesOnlyLabel] +] as const); + +const interactionStates = cartesianProduct([ + [disabledStateIsEnabled], + appearanceStates, + appearanceVariantStates, + [partVisibilityStatesOnlyLabel] +] as const); + +export const anchorButtonInteractionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + export const hiddenAnchorButton: StoryFn = createStory( hiddenWrapper( html`<${anchorButtonTag} hidden diff --git a/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts b/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts index 255bc21f20..8d05a24f04 100644 --- a/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts +++ b/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts @@ -4,7 +4,9 @@ import { pascalCase } from '@microsoft/fast-web-utilities'; import { createMatrix, sharedMatrixParameters, - createMatrixThemeStory + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates } from '../../utilities/tests/matrix'; import { createStory } from '../../utilities/tests/storybook'; import { hiddenWrapper } from '../../utilities/tests/hidden'; @@ -12,6 +14,11 @@ import { textCustomizationWrapper } from '../../utilities/tests/text-customizati import { AnchorAppearance } from '../types'; import { bodyFont } from '../../theme-provider/design-tokens'; import { anchorTag } from '..'; +import { + disabledStates, + type DisabledState, + disabledStateIsEnabled +} from '../../utilities/tests/states'; const metadata: Meta = { title: 'Tests/Anchor', @@ -22,12 +29,6 @@ const metadata: Meta = { export default metadata; -const disabledStates = [ - ['', 'https://nimble.ni.dev'], - ['Disabled', null] -] as const; -type DisabledState = (typeof disabledStates)[number]; - const underlineHiddenStates = [ ['', false], ['Underline Hidden', true] @@ -41,12 +42,12 @@ type AppearanceState = (typeof appearanceStates)[number]; // prettier-ignore const component = ( - [disabledName, href]: DisabledState, + [disabledName, disabled]: DisabledState, [underlineHiddenName, underlineHidden]: UnderlineHiddenState, [appearanceName, appearance]: AppearanceState ): ViewTemplate => html` <${anchorTag} - href=${() => href} + href=${() => (disabled ? undefined : 'https://nimble.ni.dev')} ?underline-hidden="${() => underlineHidden}" appearance="${() => appearance}" style="margin-right: 8px; margin-bottom: 8px;"> @@ -61,6 +62,27 @@ export const anchorThemeMatrix: StoryFn = createMatrixThemeStory( ]) ); +const interactionStatesHover = cartesianProduct([ + disabledStates, + underlineHiddenStates, + appearanceStates +] as const); + +const interactionStates = cartesianProduct([ + [disabledStateIsEnabled], + underlineHiddenStates, + appearanceStates +] as const); + +export const anchorInteractionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + export const hiddenAnchor: StoryFn = createStory( hiddenWrapper(html`<${anchorTag} hidden>Hidden Anchor`) ); diff --git a/packages/nimble-components/src/button/tests/button-matrix.stories.ts b/packages/nimble-components/src/button/tests/button-matrix.stories.ts index fc0e33694d..2b0231ede1 100644 --- a/packages/nimble-components/src/button/tests/button-matrix.stories.ts +++ b/packages/nimble-components/src/button/tests/button-matrix.stories.ts @@ -3,9 +3,15 @@ import { html, ViewTemplate, when } from '@microsoft/fast-element'; import { createMatrix, sharedMatrixParameters, - createMatrixThemeStory + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates } from '../../utilities/tests/matrix'; -import { disabledStates, DisabledState } from '../../utilities/tests/states'; +import { + disabledStates, + DisabledState, + disabledStateIsEnabled +} from '../../utilities/tests/states'; import { createStory } from '../../utilities/tests/storybook'; import { hiddenWrapper } from '../../utilities/tests/hidden'; import { textCustomizationWrapper } from '../../utilities/tests/text-customization'; @@ -19,7 +25,8 @@ import { type AppearanceVariantState, type PartVisibilityState, appearanceVariantStates, - partVisibilityStates + partVisibilityStates, + partVisibilityStatesOnlyLabel } from '../../patterns/button/tests/states'; const metadata: Meta = { @@ -59,6 +66,29 @@ export const buttonThemeMatrix: StoryFn = createMatrixThemeStory( ]) ); +const interactionStates = cartesianProduct([ + [disabledStateIsEnabled], + appearanceStates, + appearanceVariantStates, + [partVisibilityStatesOnlyLabel] +] as const); + +const interactionStatesHover = cartesianProduct([ + disabledStates, + appearanceStates, + appearanceVariantStates, + [partVisibilityStatesOnlyLabel] +] as const); + +export const buttonInteractionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + export const hiddenButton: StoryFn = createStory( hiddenWrapper(html`<${buttonTag} hidden>Hidden Button`) ); diff --git a/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts b/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts index 14f7d570cd..65a5598338 100644 --- a/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts +++ b/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts @@ -3,23 +3,28 @@ import { html, ViewTemplate, when } from '@microsoft/fast-element'; import { createMatrix, sharedMatrixParameters, - createMatrixThemeStory + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates } from '../../utilities/tests/matrix'; -import { disabledStates, DisabledState } from '../../utilities/tests/states'; +import { + disabledStates, + DisabledState, + disabledStateIsEnabled +} from '../../utilities/tests/states'; import { createStory } from '../../utilities/tests/storybook'; import { hiddenWrapper } from '../../utilities/tests/hidden'; import { iconArrowExpanderDownTag } from '../../icons/arrow-expander-down'; import { iconKeyTag } from '../../icons/key'; import { menuButtonTag } from '..'; -import { menuTag } from '../../menu'; -import { menuItemTag } from '../../menu-item'; import { appearanceStates, type AppearanceState, type AppearanceVariantState, type PartVisibilityState, appearanceVariantStates, - partVisibilityStates + partVisibilityStates, + partVisibilityStatesOnlyLabel } from '../../patterns/button/tests/states'; const metadata: Meta = { @@ -31,8 +36,15 @@ const metadata: Meta = { export default metadata; +const openStates = [ + ['', false], + ['Open', true] +] as const; +type OpenState = (typeof openStates)[number]; + // prettier-ignore const component = ( + [openName, open]: OpenState, [iconVisible, labelVisible, endIconVisible]: PartVisibilityState, [disabledName, disabled]: DisabledState, [appearanceName, appearance]: AppearanceState, @@ -41,22 +53,19 @@ const component = ( <${menuButtonTag} appearance="${() => appearance}" appearance-variant="${() => appearanceVariant}" + ?open="${() => open}" ?disabled=${() => disabled} ?content-hidden=${() => !labelVisible} style="margin-right: 8px; margin-bottom: 8px;"> ${when(() => iconVisible, html`<${iconKeyTag} slot="start">`)} - ${() => `${appearanceVariantName} ${appearanceName} Menu Button ${disabledName}`} + ${() => `${openName} ${appearanceVariantName} ${appearanceName} Menu Button ${disabledName}`} ${when(() => endIconVisible, html`<${iconArrowExpanderDownTag} slot="end">`)} - - <${menuTag} slot="menu"> - <${menuItemTag}>Item 1 - <${menuItemTag}>Item 2 - `; export const menuButtonThemeMatrix: StoryFn = createMatrixThemeStory( createMatrix(component, [ + openStates, partVisibilityStates, disabledStates, appearanceStates, @@ -64,6 +73,31 @@ export const menuButtonThemeMatrix: StoryFn = createMatrixThemeStory( ]) ); +const interactionStatesHover = cartesianProduct([ + openStates, + [partVisibilityStatesOnlyLabel], + disabledStates, + appearanceStates, + appearanceVariantStates +] as const); + +const interactionStates = cartesianProduct([ + openStates, + [partVisibilityStatesOnlyLabel], + [disabledStateIsEnabled], + appearanceStates, + appearanceVariantStates +] as const); + +export const menuButtonInteractionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + export const hiddenMenuButton: StoryFn = createStory( hiddenWrapper( html`<${menuButtonTag} hidden>Hidden Menu Button` diff --git a/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts b/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts index a6f3f35220..56ea4cbcc2 100644 --- a/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts +++ b/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts @@ -5,13 +5,18 @@ import { createStory } from '../../utilities/tests/storybook'; import { createMatrixThemeStory, createMatrix, - sharedMatrixParameters + sharedMatrixParameters, + cartesianProduct, + createMatrixInteractionsFromStates } from '../../utilities/tests/matrix'; import { disabledStates, DisabledState, errorStates, - ErrorState + ErrorState, + disabledStateIsEnabled, + errorStatesNoError, + errorStatesErrorWithMessage } from '../../utilities/tests/states'; import { hiddenWrapper } from '../../utilities/tests/hidden'; import { NumberFieldAppearance } from '../types'; @@ -31,6 +36,7 @@ const valueStates = [ ['Value', '1234', null] ] as const; type ValueState = (typeof valueStates)[number]; +const valueStatesHasValue = valueStates[1]; const appearanceStates = Object.entries(NumberFieldAppearance).map( ([key, value]) => [pascalCase(key), value] @@ -42,6 +48,7 @@ const hideStepStates = [ ['Hide Step', true] ] as const; type HideStepState = (typeof hideStepStates)[number]; +const hideStepStateStepVisible = hideStepStates[0]; const component = ( [disabledName, disabled]: DisabledState, @@ -52,7 +59,6 @@ const component = ( ): ViewTemplate => html` <${numberFieldTag} style="width: 250px; padding: 8px;" - class="${() => errorVisible}" value="${() => valueValue}" placeholder="${() => placeholderValue}" appearance="${() => appearance}" @@ -76,6 +82,31 @@ export const numberFieldThemeMatrix: StoryFn = createMatrixThemeStory( ]) ); +const interactionStatesHover = cartesianProduct([ + disabledStates, + [hideStepStateStepVisible], + [valueStatesHasValue], + [errorStatesNoError, errorStatesErrorWithMessage], + appearanceStates +] as const); + +const interactionStates = cartesianProduct([ + [disabledStateIsEnabled], + [hideStepStateStepVisible], + [valueStatesHasValue], + [errorStatesNoError, errorStatesErrorWithMessage], + appearanceStates +] as const); + +export const numberFieldInteractionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + export const hiddenNumberField: StoryFn = createStory( hiddenWrapper( html` diff --git a/packages/nimble-components/src/patterns/button/tests/states.ts b/packages/nimble-components/src/patterns/button/tests/states.ts index 7ea753741f..bbc0eb425b 100644 --- a/packages/nimble-components/src/patterns/button/tests/states.ts +++ b/packages/nimble-components/src/patterns/button/tests/states.ts @@ -9,6 +9,7 @@ export const partVisibilityStates = [ [false, true, true] ] as const; export type PartVisibilityState = (typeof partVisibilityStates)[number]; +export const partVisibilityStatesOnlyLabel = partVisibilityStates[2]; export const appearanceStates = [ ['Outline', ButtonAppearance.outline], diff --git a/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts b/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts index 7e6fc7e002..1be9058b29 100644 --- a/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts +++ b/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts @@ -3,9 +3,15 @@ import { html, ViewTemplate, when } from '@microsoft/fast-element'; import { createMatrix, sharedMatrixParameters, - createMatrixThemeStory + createMatrixThemeStory, + cartesianProduct, + createMatrixInteractionsFromStates } from '../../utilities/tests/matrix'; -import { disabledStates, DisabledState } from '../../utilities/tests/states'; +import { + disabledStates, + DisabledState, + disabledStateIsEnabled +} from '../../utilities/tests/states'; import { createStory } from '../../utilities/tests/storybook'; import { hiddenWrapper } from '../../utilities/tests/hidden'; import { textCustomizationWrapper } from '../../utilities/tests/text-customization'; @@ -18,7 +24,8 @@ import { type AppearanceVariantState, type PartVisibilityState, appearanceVariantStates, - partVisibilityStates + partVisibilityStates, + partVisibilityStatesOnlyLabel } from '../../patterns/button/tests/states'; const metadata: Meta = { @@ -67,6 +74,31 @@ export const toggleButtonThemeMatrix: StoryFn = createMatrixThemeStory( ]) ); +const interactionStatesHover = cartesianProduct([ + [partVisibilityStatesOnlyLabel], + checkedStates, + disabledStates, + appearanceStates, + appearanceVariantStates +] as const); + +const interactionStates = cartesianProduct([ + [partVisibilityStatesOnlyLabel], + checkedStates, + [disabledStateIsEnabled], + appearanceStates, + appearanceVariantStates +] as const); + +export const toggleButtonInteractionsThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrixInteractionsFromStates(component, { + hover: interactionStatesHover, + hoverActive: interactionStates, + active: interactionStates, + focus: interactionStates + }) +); + export const hiddenButton: StoryFn = createStory( hiddenWrapper( html`<${toggleButtonTag} hidden diff --git a/packages/nimble-components/src/utilities/tests/matrix.ts b/packages/nimble-components/src/utilities/tests/matrix.ts index c98dc83b0b..1e641a4592 100644 --- a/packages/nimble-components/src/utilities/tests/matrix.ts +++ b/packages/nimble-components/src/utilities/tests/matrix.ts @@ -2,6 +2,7 @@ import { html, repeat, ViewTemplate } from '@microsoft/fast-element'; import { fastParameters, renderViewTemplate } from './storybook'; import { themeProviderTag } from '../../theme-provider'; import { type BackgroundState, backgroundStates } from './states'; +import { bodyFont, bodyFontColor } from '../../theme-provider/design-tokens'; export const sharedMatrixParameters = () => ({ ...fastParameters(), @@ -23,182 +24,15 @@ export const sharedMatrixParameters = () => ({ } }) as const; +type MakeTupleEntriesArrays = { [K in keyof T]: readonly T[K][] }; + /** - * Takes an array of state values that can be used with the template to match the permutations of the provided states. + * Calculates the cartesian product of an array of sets. */ -export function createMatrix(component: () => ViewTemplate): ViewTemplate; - -export function createMatrix( - component: (state1: State1) => ViewTemplate, - dimensions: readonly [readonly State1[]] -): ViewTemplate; - -export function createMatrix( - component: (state1: State1, state2: State2) => ViewTemplate, - dimensions: readonly [readonly State1[], readonly State2[]] -): ViewTemplate; - -export function createMatrix( - component: (state1: State1, state2: State2, state3: State3) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[] - ] -): ViewTemplate; - -export function createMatrix( - component: ( - state1: State1, - state2: State2, - state3: State3, - state4: State4 - ) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[], - readonly State4[] - ] -): ViewTemplate; - -export function createMatrix( - component: ( - state1: State1, - state2: State2, - state3: State3, - state4: State4, - state5: State5 - ) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[], - readonly State4[], - readonly State5[] - ] -): ViewTemplate; - -export function createMatrix( - component: ( - state1: State1, - state2: State2, - state3: State3, - state4: State4, - state5: State5, - state6: State6 - ) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[], - readonly State4[], - readonly State5[], - readonly State6[] - ] -): ViewTemplate; - -export function createMatrix< - State1, - State2, - State3, - State4, - State5, - State6, - State7 ->( - component: ( - state1: State1, - state2: State2, - state3: State3, - state4: State4, - state5: State5, - state6: State6, - state7: State7 - ) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[], - readonly State4[], - readonly State5[], - readonly State6[], - readonly State7[] - ] -): ViewTemplate; - -export function createMatrix< - State1, - State2, - State3, - State4, - State5, - State6, - State7, - State8 ->( - component: ( - state1: State1, - state2: State2, - state3: State3, - state4: State4, - state5: State5, - state6: State6, - state7: State7, - state8: State8 - ) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[], - readonly State4[], - readonly State5[], - readonly State6[], - readonly State7[], - readonly State8[] - ] -): ViewTemplate; - -export function createMatrix< - State1, - State2, - State3, - State4, - State5, - State6, - State7, - State8, - State9 ->( - component: ( - state1: State1, - state2: State2, - state3: State3, - state4: State4, - state5: State5, - state6: State6, - state7: State7, - state8: State8, - state9: State9 - ) => ViewTemplate, - dimensions: readonly [ - readonly State1[], - readonly State2[], - readonly State3[], - readonly State4[], - readonly State5[], - readonly State6[], - readonly State7[], - readonly State8[], - readonly State9[] - ] -): ViewTemplate; - -export function createMatrix( - component: (...states: readonly unknown[]) => ViewTemplate, - dimensions?: readonly (readonly unknown[])[] -): ViewTemplate { - const matrix: ViewTemplate[] = []; +export function cartesianProduct( + dimensions?: MakeTupleEntriesArrays +): [...T][] { + const result: [...T][] = []; const recurseDimensions = ( currentDimensions?: readonly (readonly unknown[])[], ...states: readonly unknown[] @@ -209,16 +43,37 @@ export function createMatrix( recurseDimensions(remainingDimensions, ...states, currentState); } } else { - matrix.push(component(...states)); + result.push(states as [...T]); } }; recurseDimensions(dimensions); + return result; +} + +/** + * Passes each of the given state combinations into a template function and returns the combined output. + */ +function createMatrixFromStates( + component: (...states: T) => ViewTemplate, + states: T[] +): ViewTemplate { // prettier-ignore return html` - ${repeat(() => matrix, html` - ${(x: ViewTemplate): ViewTemplate => x} - `)} - `; + ${repeat(() => states, html` + ${(x: T): ViewTemplate => component(...x)} + `)} +`; +} + +/** + * Creates a template that renders all combinations of states in the given dimensions. + */ +export function createMatrix( + component: (...states: T) => ViewTemplate, + dimensions?: MakeTupleEntriesArrays +): ViewTemplate { + const states = cartesianProduct(dimensions); + return createMatrixFromStates(component, states); } /** @@ -247,3 +102,45 @@ export const createMatrixThemeStory = ( return content; }; }; + +export function createMatrixInteractionsFromStates< + THover extends readonly unknown[], + THoverActive extends readonly unknown[], + TActive extends readonly unknown[], + TFocus extends readonly unknown[] +>( + component: ( + ...states: THover | TActive | THoverActive | TFocus + ) => ViewTemplate, + states: { + hover: THover[], + hoverActive: THoverActive[], + active: TActive[], + focus: TFocus[] + } +): ViewTemplate { + // prettier-ignore + return html` +
+
+

Hover

+ ${createMatrixFromStates(component, states.hover)} +
+
+

Hover and active

+ ${createMatrixFromStates(component, states.hoverActive)} +
+
+

Active

+ ${createMatrixFromStates(component, states.active)} +
+
+

Focus

+ ${createMatrixFromStates(component, states.focus)} +
+
+`; +} diff --git a/packages/nimble-components/src/utilities/tests/states.ts b/packages/nimble-components/src/utilities/tests/states.ts index bbe9820e11..91f0c29afb 100644 --- a/packages/nimble-components/src/utilities/tests/states.ts +++ b/packages/nimble-components/src/utilities/tests/states.ts @@ -25,6 +25,7 @@ export const disabledStates = [ ['Disabled', true] ] as const; export type DisabledState = (typeof disabledStates)[number]; +export const disabledStateIsEnabled = disabledStates[0]; export const errorStates = [ ['', false, ''], @@ -32,6 +33,9 @@ export const errorStates = [ ['Error No Message', true, ''] ] as const; export type ErrorState = (typeof errorStates)[number]; +export const errorStatesNoError = errorStates[0]; +export const errorStatesErrorWithMessage = errorStates[1]; +export const errorStatesErrorNoMessage = errorStates[2]; export const readOnlyStates = [ ['', false],