Skip to content

Commit

Permalink
Add interaction stories for buttons, number field, and anchor (#1919)
Browse files Browse the repository at this point in the history
## 🀨 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-<state>-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 <rajsite@users.noreply.github.com>
  • Loading branch information
m-akinc and rajsite authored Mar 27, 2024
1 parent a15adbb commit df7f14c
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 213 deletions.
Original file line number Diff line number Diff line change
@@ -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"
}
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/nimble-components/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
5 changes: 4 additions & 1 deletion packages/nimble-components/.storybook/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ addons.setConfig({
}
}
},
theme
theme,
toolbar: {
'storybook/pseudo-states/tool': { hidden: true }
}
});
1 change: 1 addition & 0 deletions packages/nimble-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,7 +24,8 @@ import {
type AppearanceVariantState,
type PartVisibilityState,
appearanceVariantStates,
partVisibilityStates
partVisibilityStates,
partVisibilityStatesOnlyLabel
} from '../../patterns/button/tests/states';

const metadata: Meta = {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ 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';
import { textCustomizationWrapper } from '../../utilities/tests/text-customization';
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',
Expand All @@ -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]
Expand All @@ -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;">
Expand All @@ -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</${anchorTag}>`)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,7 +25,8 @@ import {
type AppearanceVariantState,
type PartVisibilityState,
appearanceVariantStates,
partVisibilityStates
partVisibilityStates,
partVisibilityStatesOnlyLabel
} from '../../patterns/button/tests/states';

const metadata: Meta = {
Expand Down Expand Up @@ -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</${buttonTag}>`)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -41,29 +53,51 @@ 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"></${iconKeyTag}>`)}
${() => `${appearanceVariantName} ${appearanceName} Menu Button ${disabledName}`}
${() => `${openName} ${appearanceVariantName} ${appearanceName} Menu Button ${disabledName}`}
${when(() => endIconVisible, html`<${iconArrowExpanderDownTag} slot="end"></${iconArrowExpanderDownTag}>`)}
<${menuTag} slot="menu">
<${menuItemTag}>Item 1</${menuItemTag}>
<${menuItemTag}>Item 2</${menuItemTag}>
</${menuTag}>
</${menuButtonTag}>
`;

export const menuButtonThemeMatrix: StoryFn = createMatrixThemeStory(
createMatrix(component, [
openStates,
partVisibilityStates,
disabledStates,
appearanceStates,
appearanceVariantStates
])
);

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</${menuButtonTag}>`
Expand Down
Loading

0 comments on commit df7f14c

Please sign in to comment.