Skip to content

Commit

Permalink
Merge branch 'main' into feature/labeled-field
Browse files Browse the repository at this point in the history
  • Loading branch information
beaesguerra committed Dec 11, 2024
2 parents 4aceede + 0955be7 commit 309adcb
Show file tree
Hide file tree
Showing 147 changed files with 7,424 additions and 1,762 deletions.
7 changes: 7 additions & 0 deletions .changeset/kind-buckets-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@khanacademy/wonder-blocks-modal": patch
---

- ModalBackdrop: Change initial focus behavior. Focus on the dismiss button (X) by default.

- CloseButton: Override `:focus` styles on the dismiss button to make it visually distinct when the focus is set programmatically.
5 changes: 5 additions & 0 deletions .changeset/perfect-cameras-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-icon-button": patch
---

Fix focus styles: drop Safari v14 support.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ module.exports = {
"no-undef": "off",
},
},
{
files: ["**/*.stories.tsx"],
rules: {
"testing-library/no-await-sync-events": "off",
},
},
],
globals: {
// `no-undef` doesn't support `globalThis`, for details see
Expand Down
3 changes: 2 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const parameters = {
},
};

export const decorators = [
const decorators = [
(Story, context) => {
const theme = context.globals.theme;
const enableRenderStateRootDecorator =
Expand All @@ -120,6 +120,7 @@ export const decorators = [

const preview: Preview = {
parameters,
decorators,
globalTypes: {
// Allow the user to select a theme from the toolbar.
theme: {
Expand Down
31 changes: 17 additions & 14 deletions __docs__/wonder-blocks-button/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from "react";
import {StyleSheet} from "aphrodite";
import type {Meta, StoryObj} from "@storybook/react";
import {expect, fireEvent, userEvent, within} from "@storybook/test";
import {expect, userEvent, within} from "@storybook/test";

import {MemoryRouter, Route, Switch} from "react-router-dom";

Expand Down Expand Up @@ -103,7 +103,6 @@ export const Tertiary: StoryComponentType = {

// Get HTML elements
const button = canvas.getByRole("button");
const computedStyleButton = getComputedStyle(button);
const innerLabel = canvas.getByTestId("test-button-inner-label");
const computedStyleLabel = getComputedStyle(innerLabel, ":after");

Expand All @@ -116,19 +115,23 @@ export const Tertiary: StoryComponentType = {
await expect(computedStyleLabel.height).toBe("2px");
await expect(computedStyleLabel.color).toBe("rgb(24, 101, 242)");

// TODO(WB-1808, somewhatabstract): This isn't working. I got it passing
// locally by calling `button.focus()` as well, but it was super flaky
// and never passed first time.
// Focus style
await fireEvent.focus(button);
await expect(computedStyleButton.outlineColor).toBe(
"rgb(24, 101, 242)",
);
await expect(computedStyleButton.outlineWidth).toBe("2px");

// Active (mouse down) style
// eslint-disable-next-line testing-library/prefer-user-event
await fireEvent.mouseDown(button);
await expect(innerLabel).toHaveStyle("color: rgb(27, 80, 179)");
await expect(computedStyleLabel.height).toBe("2px");
await expect(computedStyleLabel.color).toBe("rgb(27, 80, 179)");
// const computedStyleButton = getComputedStyle(button);
// await fireEvent.focus(button);
// await expect(computedStyleButton.outlineColor).toBe(
// "rgb(24, 101, 242)",
// );
// await expect(computedStyleButton.outlineWidth).toBe("2px");

// // Active (mouse down) style
// // eslint-disable-next-line testing-library/prefer-user-event
// await fireEvent.mouseDown(button);
// await expect(innerLabel).toHaveStyle("color: rgb(27, 80, 179)");
// await expect(computedStyleLabel.color).toBe("rgb(27, 80, 179)");
// await expect(computedStyleLabel.height).toBe("2px");
},
};

Expand Down
42 changes: 41 additions & 1 deletion __docs__/wonder-blocks-dropdown/base-select.argtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,53 @@ const argTypes: ArgTypes = {
},

error: {
description: "Whether this component is in an error state.",
description: `Whether this component is in an error state. Use this for
errors that are triggered by something external to the component
(example: an error after form submission).`,
table: {
category: "States",
defaultValue: {summary: "false"},
},
},

required: {
description: `Whether this field is required to to continue, or the
error message to render if the select is left blank. Pass in a
message instead of "true" if possible.`,
table: {
category: "States",
type: {
summary: "boolean | string",
},
},
control: {
type: undefined,
},
},

validate: {
description: `Provide a validation for the selected value. Return a
string error message or null | void for a valid input.
\n Use this for errors that are shown to the user while they are
filling out a form.`,
table: {
category: "States",
type: {
summary: "(value: string) => ?string",
},
},
},

onValidate: {
description: "Called right after the field is validated.",
table: {
category: "Events",
type: {
summary: "(errorMessage: ?string) => mixed",
},
},
},

isFilterable: {
description: `When this is true, the dropdown body shows a search text
input top. The items will be filtered by the input. Selected items
Expand Down
1 change: 1 addition & 0 deletions __docs__/wonder-blocks-dropdown/multi-select.argtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const argTypes: ArgTypes = {
table: {
type: {summary: "Array<string>"},
},
control: {type: "object"},
},
labels: {
control: {type: "object"},
Expand Down
149 changes: 120 additions & 29 deletions __docs__/wonder-blocks-dropdown/multi-select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {StyleSheet} from "aphrodite";

import {action} from "@storybook/addon-actions";
import type {Meta, StoryObj} from "@storybook/react";
import {View} from "@khanacademy/wonder-blocks-core";
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";

import Button from "@khanacademy/wonder-blocks-button";
import {Checkbox} from "@khanacademy/wonder-blocks-form";
import {OnePaneDialog, ModalLauncher} from "@khanacademy/wonder-blocks-modal";
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import {color, semanticColor, spacing} from "@khanacademy/wonder-blocks-tokens";
import {HeadingLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography";
import {MultiSelect, OptionItem} from "@khanacademy/wonder-blocks-dropdown";
import Pill from "@khanacademy/wonder-blocks-pill";
Expand Down Expand Up @@ -129,14 +129,14 @@ const styles = StyleSheet.create({
});

const items = [
<OptionItem label="Mercury" value="1" key={1} />,
<OptionItem label="Venus" value="2" key={2} />,
<OptionItem label="Earth" value="3" disabled key={3} />,
<OptionItem label="Mars" value="4" key={4} />,
<OptionItem label="Jupiter" value="5" key={5} />,
<OptionItem label="Saturn" value="6" key={6} />,
<OptionItem label="Neptune" value="7" key={7} />,
<OptionItem label="Uranus" value="8" key={8} />,
<OptionItem label="Mercury" value="mercury" key={1} />,
<OptionItem label="Venus" value="venus" key={2} />,
<OptionItem label="Earth" value="earth" disabled key={3} />,
<OptionItem label="Mars" value="mars" key={4} />,
<OptionItem label="Jupiter" value="jupiter" key={5} />,
<OptionItem label="Saturn" value="saturn" key={6} />,
<OptionItem label="Neptune" value="neptune" key={7} />,
<OptionItem label="Uranus" value="uranus" key={8} />,
];

const Template = (args: any) => {
Expand Down Expand Up @@ -271,42 +271,133 @@ export const CustomStylesOpened: StoryComponentType = {
],
};

const ErrorWrapper = (args: any) => {
const [selectedValues, setSelectedValues] = React.useState<string[]>([]);
const ControlledMultiSelect = (args: PropsFor<typeof MultiSelect>) => {
const [opened, setOpened] = React.useState(false);
const [error, setError] = React.useState(true);

const [selectedValues, setSelectedValues] = React.useState<string[]>(
args.selectedValues || [],
);
const [errorMessage, setErrorMessage] = React.useState<
null | string | void
>(null);
return (
<>
<LabelMedium style={{marginBottom: spacing.xSmall_8}}>
Select at least 2 options to clear the error!
</LabelMedium>
<View style={{gap: spacing.xSmall_8}}>
<MultiSelect
{...args}
error={error}
onChange={(values) => {
setSelectedValues(values);
setError(values.length < 2);
}}
onToggle={setOpened}
id="multi-select"
opened={opened}
onToggle={setOpened}
selectedValues={selectedValues}
onChange={setSelectedValues}
validate={(values) => {
if (values.includes("jupiter")) {
return "Don't pick jupiter!";
}
}}
onValidate={setErrorMessage}
>
{items}
</MultiSelect>
</>
{(errorMessage || args.error) && (
<LabelMedium
style={{color: semanticColor.status.critical.foreground}}
>
{errorMessage || "Error from error prop"}
</LabelMedium>
)}
</View>
);
};

/**
* Here is an example of a dropdown that is in an error state. Selecting two or
* more options will clear the error by setting the `error` prop to `false`.
* If the `error` prop is set to true, the field will have error styling and
* `aria-invalid` set to `true`.
*
* This is useful for scenarios where we want to show an error on a
* specific field after a form is submitted (server validation).
*
* Note: The `required` and `validate` props can also put the field in an
* error state.
*/
export const Error: StoryComponentType = {
render: ErrorWrapper,
render: ControlledMultiSelect,
args: {
error: true,
} as MultiSelectArgs,
},
parameters: {
chromatic: {
// Disabling because this is covered by variants story
disableSnapshot: true,
},
},
};

/**
* A required field will have error styling and aria-invalid set to true if the
* select is left blank.
*
* When `required` is set to `true`, validation is triggered:
* - When a user tabs away from the select (opener's onBlur event)
* - When a user closes the dropdown without selecting a value
* (either by pressing escape, clicking away, or clicking on the opener).
*
* Validation errors are cleared when a valid value is selected. The component
* will set aria-invalid to "false" and call the onValidate prop with null.
*
*/
export const Required: StoryComponentType = {
render: ControlledMultiSelect,
args: {
required: "Custom required error message",
},
parameters: {
chromatic: {
// Disabling because this doesn't test anything visual.
disableSnapshot: true,
},
},
};

/**
* If a selected value fails validation, the field will have error styling.
*
* This is useful for scenarios where we want to show errors while a
* user is filling out a form (client validation).
*
* Note that we will internally set the correct `aria-invalid` attribute to the
* field:
* - aria-invalid="true" if there is an error.
* - aria-invalid="false" if there is no error.
*
* Validation is triggered:
* - On mount if the `value` prop is not empty and it is not required
* - When the dropdown is closed after updating the selected values
*
* Validation errors are cleared when the value is updated. The component
* will set aria-invalid to "false" and call the onValidate prop with null.
*/
export const ErrorFromValidation: StoryComponentType = {
render: (args: PropsFor<typeof MultiSelect>) => {
return (
<View style={{gap: spacing.xSmall_8}}>
<LabelMedium htmlFor="multi-select" tag="label">
Validation example (try picking jupiter)
</LabelMedium>
<ControlledMultiSelect {...args} id="multi-select">
{items}
</ControlledMultiSelect>
<LabelMedium htmlFor="multi-select" tag="label">
Validation example (on mount)
</LabelMedium>
<ControlledMultiSelect
{...args}
selectedValues={["jupiter"]}
id="multi-select"
>
{items}
</ControlledMultiSelect>
</View>
);
},
};

/**
Expand Down
Loading

0 comments on commit 309adcb

Please sign in to comment.