diff --git a/.github/workflows/storybook_test.yaml b/.github/workflows/storybook_test.yaml index 7687e2a5..24d2d52a 100644 --- a/.github/workflows/storybook_test.yaml +++ b/.github/workflows/storybook_test.yaml @@ -5,11 +5,11 @@ name: Storybook Tests on: push: - branches: [ "**" ] + branches: + - "master" jobs: build: - runs-on: ubuntu-latest strategy: diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index cd95a006..36647aff 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -14,6 +14,9 @@ const config: TestRunnerConfig = { await injectAxe(page); }, async postVisit(page, context) { + // Workaround for https://github.com/dequelabs/axe-core/issues/3426 + await new Promise(resolve => setTimeout(resolve, 200)); + // Get the entire context of a story, including parameters, args, argTypes, etc. const storyContext = await getStoryContext(page, context); diff --git a/.vscode/settings.json b/.vscode/settings.json index ad440a7a..5ce5d3ac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,4 +20,7 @@ "files.eol": "\n", "javascript.preferences.quoteStyle": "single", "typescript.tsdk": "node_modules/typescript/lib", // Use the TypeScript SDK from the current project + "typescript.preferences.autoImportFileExcludePatterns": [ + "storybook/internal/**/*" + ], } diff --git a/biome.jsonc b/biome.jsonc index ec9e15f1..14ee060a 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -23,19 +23,24 @@ }, "linter": { "enabled": true, - "include": ["app/**/*", "src/**/*", "tests/**/*"], + "include": ["app", "src", "tests"], "ignore": [ "node_modules", + "dist", "src/components/tables/MultiSearch/MultiSearch.tsx", // Ignore for now (need to focus on type errors first) - "tests/installation/**/*" - ], + "tests/installation" + ], "rules": { "recommended": true, + "correctness": { + //"useExhaustiveDependencies": "off" + }, "complexity": { "noBannedTypes": "off", "noForEach": "off", - "useOptionalChain": "off" + "useOptionalChain": "off", + "useLiteralKeys": "off" }, "style": { "useImportType": "off", diff --git a/package.json b/package.json index 302e98ab..a1786042 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "test": "npm run check:types && npm run lint:style", "test-ui": "vitest --ui", "coverage": "vitest run --coverage", - "test:storybook": "test-storybook --failOnConsole --browsers chromium", + "test:storybook": "test-storybook --failOnConsole --browsers chromium --maxWorkers=1", "test:storybook-ci": "\n npx playwright install --with-deps chromium && npx concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"npm run storybook:build --quiet && npx http-server storybook-static --port 6006 --silent\" \"npx wait-on tcp:6006 && npm run test:storybook\"\n ", "start": "npm run storybook:serve", "prepare": "npm run build" diff --git a/package.json.js b/package.json.js index 5c4567bf..7f975d15 100644 --- a/package.json.js +++ b/package.json.js @@ -85,7 +85,8 @@ const packageConfig = { // Browser automation tests // https://github.com/storybookjs/test-runner?tab=readme-ov-file#2-running-against-locally-built-storybooks-in-ci - 'test:storybook': 'test-storybook --failOnConsole --browsers chromium', // For text only: FORCE_COLOR=false + 'test:storybook': 'test-storybook --failOnConsole --browsers chromium --maxWorkers=1', // For text only: FORCE_COLOR=false + // Note: the following assumes `localhost:6006` is free, so don't run it if a dev server is already running 'test:storybook-ci': ` npx playwright install --with-deps chromium\ && npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue"\ diff --git a/src/components/actions/Link/Link.stories.tsx b/src/components/actions/Link/Link.stories.tsx index 85ce6452..bb6db4f2 100644 --- a/src/components/actions/Link/Link.stories.tsx +++ b/src/components/actions/Link/Link.stories.tsx @@ -5,9 +5,10 @@ import * as React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; +import { DummyLink } from '../../../util/storybook/StorybookLink.tsx'; +import { OverflowTester } from '../../../util/storybook/OverflowTester.tsx'; import { Link } from './Link.tsx'; -import { OverflowTester } from '../../../util/storybook/OverflowTester.tsx'; type LinkArgs = React.ComponentProps; @@ -45,7 +46,7 @@ export const Descenders: Story = { export const Scroll: Story = { render: (args) => ( <> - { evt.preventDefault(); }}>Anchor + Anchor diff --git a/src/components/containers/Banner/Banner.tsx b/src/components/containers/Banner/Banner.tsx index 37eb60ff..8da51f29 100644 --- a/src/components/containers/Banner/Banner.tsx +++ b/src/components/containers/Banner/Banner.tsx @@ -45,10 +45,8 @@ const ActionButton = (props: ActionButtonProps) => { diff --git a/src/components/forms/controls/Switch/Switch.stories.tsx b/src/components/forms/controls/Switch/Switch.stories.tsx index 74768484..2e1c9d21 100644 --- a/src/components/forms/controls/Switch/Switch.stories.tsx +++ b/src/components/forms/controls/Switch/Switch.stories.tsx @@ -17,6 +17,7 @@ export default { tags: ['autodocs'], argTypes: {}, args: { + 'aria-label': 'Test switch', defaultChecked: true, }, decorators: [ diff --git a/src/components/forms/controls/TimePicker/TimePicker.stories.tsx b/src/components/forms/controls/TimePicker/TimePicker.stories.tsx index 4d3a0782..22aad731 100644 --- a/src/components/forms/controls/TimePicker/TimePicker.stories.tsx +++ b/src/components/forms/controls/TimePicker/TimePicker.stories.tsx @@ -33,7 +33,7 @@ export const TimePickerStory: Story = { return (
- +

The selected time is: {time.hours}:{time.minutes}

diff --git a/src/components/navigations/Stepper/Stepper.module.scss b/src/components/navigations/Stepper/Stepper.module.scss index e9e82101..15aa9b25 100644 --- a/src/components/navigations/Stepper/Stepper.module.scss +++ b/src/components/navigations/Stepper/Stepper.module.scss @@ -8,53 +8,62 @@ .bk-stepper { @include bk.component-base(bk-stepper); + --bk-stepper-indicator-size: #{bk.rem-from-px(28)}; + display: flex; + + > ol { + display: contents; + } + &.bk-stepper--horizontal { - display: flex; - gap: bk.$spacing-9; + flex-direction: row; + column-gap: bk.$spacing-9; } &.bk-stepper--vertical { - .bk-stepper__item { - &:not(:first-child) { - margin-top: bk.$spacing-9; + flex-direction: column; + row-gap: bk.$spacing-9; - .bk-stepper__item__circle { - &::before { - position: absolute; - content: ''; - width: 0; - height: bk.$spacing-9; - top: -42px; - left: 50%; - border: 0.5px solid #{bk.$theme-stepper-border-disabled}; - } - } + // Draw a line between subsequent items + li + li .bk-stepper__item__indicator { + &::before { + content: ''; + position: absolute; + top: calc(-1 * bk.$spacing-9 - bk.$size-2); + left: calc(50% - bk.$size-2 / 2); + width: 0; + height: bk.$spacing-9; + border-left: bk.$size-2 solid bk.$theme-stepper-border-disabled; } } } .bk-stepper__item { + cursor: pointer; + display: flex; align-items: center; - color: #{bk.$theme-stepper-text-disabled}; - cursor: pointer; + color: bk.$theme-stepper-text-disabled; - .bk-stepper__item__circle { - display: flex; - align-items: center; - justify-content: center; - position: relative; + .bk-stepper__item__indicator { + position: relative; // Needed for the vertical line `position: absolute` + flex-shrink: 0; + margin-right: bk.$spacing-3; - border: 2px solid #{bk.$theme-stepper-border-disabled}; + aspect-ratio: 1; + width: var(--bk-stepper-indicator-size); + + border: bk.$size-2 solid #{bk.$theme-stepper-border-disabled}; border-radius: 50%; - width: 28px; - height: 28px; font-weight: bk.$font-weight-bold; font-size: bk.$font-size-m; - } - - .bk-stepper__item__circle__icon { - font-size: bk.$font-size-xs; + + display: grid; + place-content: center; + + .bk-stepper__item__indicator__icon { + font-size: bk.$font-size-xs; + } } .bk-stepper__item__title { @@ -65,22 +74,26 @@ margin-left: bk.$spacing-2; font-size: bk.$font-size-xs; } - - &[aria-selected="true"] { - color: #{bk.$theme-stepper-text-selected}; - - .bk-stepper__item__circle { - border-color: #{bk.$theme-stepper-border-default}; - background-color: #{bk.$theme-stepper-background-default}; - color: #{bk.$theme-stepper-text-selected-number}; - } - } + } + + // Any steps we've already visited + .bk-stepper__item--checked { + color: bk.$theme-stepper-text-selected; - &.bk-stepper__item--checked { - color: #{bk.$theme-stepper-text-selected}; + .bk-stepper__item__indicator { + border-color: bk.$theme-stepper-border-default; + } + } + + // The currently active step + [aria-current="true"] { + .bk-stepper__item { + color: bk.$theme-stepper-text-selected; - .bk-stepper__item__circle { - border-color: #{bk.$theme-stepper-border-default}; + .bk-stepper__item__indicator { + border-color: bk.$theme-stepper-border-default; + background-color: bk.$theme-stepper-background-default; + color: bk.$theme-stepper-text-selected-number; } } } diff --git a/src/components/navigations/Stepper/Stepper.stories.tsx b/src/components/navigations/Stepper/Stepper.stories.tsx index 8bea4946..1bb25695 100644 --- a/src/components/navigations/Stepper/Stepper.stories.tsx +++ b/src/components/navigations/Stepper/Stepper.stories.tsx @@ -6,7 +6,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import * as React from 'react'; -import { Stepper, Step } from './Stepper.tsx'; +import { type Step, Stepper } from './Stepper.tsx'; type StepperArgs = React.ComponentProps; @@ -27,7 +27,7 @@ export default { const defaultSteps: Step[] = [1,2,3,4].map(index => { return { stepKey: `${index}`, - title: `Step${index}`, + title: `Step ${index}`, isOptional: index === 4, }; }); diff --git a/src/components/navigations/Stepper/Stepper.tsx b/src/components/navigations/Stepper/Stepper.tsx index 7700790e..8f1a5aa3 100644 --- a/src/components/navigations/Stepper/Stepper.tsx +++ b/src/components/navigations/Stepper/Stepper.tsx @@ -6,9 +6,18 @@ import * as React from 'react'; import { type ClassNameArgument, type ComponentProps, classNames as cx } from '../../../util/componentUtil.ts'; import { Icon } from '../../graphics/Icon/Icon.tsx'; +import { Button } from '../../actions/Button/Button.tsx'; import cl from './Stepper.module.scss'; + +/* +References: +- https://stackoverflow.com/questions/52932018/making-a-step-progress-indicator-accessible-for-screen-readers +- https://www.telerik.com/design-system/docs/components/stepper/accessibility +- https://cauldron.dequelabs.com/components/Stepper +*/ + export { cl as SteppersClassNames }; export type Step = { @@ -18,23 +27,21 @@ export type Step = { hide?: boolean, isOptional?: boolean, }; - export type StepperKey = Step['stepKey']; - export type StepperDirection = 'vertical' | 'horizontal'; -export type StepperProps = React.PropsWithChildren & { +export type StepperProps = React.PropsWithChildren & { /** Whether this component should be unstyled. */ unstyled?: undefined | boolean, /** Step items. */ - steps: Step[], + steps: Array, /** Active key of step. */ - activeKey?: string, + activeKey?: undefined | string, /** Whether this component should be displayed vertically or horizontally. */ - direction?: StepperDirection, + direction?: undefined | StepperDirection, /** Callback executed when active step is changed. */ onSwitch: (stepKey: StepperKey) => void, @@ -45,51 +52,49 @@ export type StepperProps = React.PropsWithChildren & { export const Stepper = (props: StepperProps) => { const { unstyled = false, steps = [], activeKey, direction = 'vertical', onSwitch, ...propsRest } = props; - const handleKeyDown = (event: React.KeyboardEvent, stepKey: Step['stepKey']) => { - if (event.key === 'Enter') { - onSwitch(stepKey); - } - }; - return ( -
    - {steps.map((step, index) => { - if (step.hide) return null; - const isActive = step.stepKey === activeKey; - const isChecked = index < steps.findIndex(step => step.stepKey === activeKey); - return ( -
  • { onSwitch(step.stepKey); }} - onKeyDown={(event) => { handleKeyDown(event, step.stepKey); }} - > - - {isChecked - ? - : index + 1 - } - - {step.title} - {step.isOptional && (Optional)} -
  • - ) - })} -
+
    + {steps.map((step, index) => { + if (step.hide) return null; + const isActive = step.stepKey === activeKey; + const stepNumber = index + 1; + const isChecked = index < steps.findIndex(step => step.stepKey === activeKey); + return ( +
  1. + +
  2. + ) + })} +
+ ); }; diff --git a/src/components/navigations/Tabs/Tabs.stories.tsx b/src/components/navigations/Tabs/Tabs.stories.tsx index 8b17a999..d59e1b7d 100644 --- a/src/components/navigations/Tabs/Tabs.stories.tsx +++ b/src/components/navigations/Tabs/Tabs.stories.tsx @@ -35,12 +35,14 @@ const defaultTabOptions: DefaultTabOption[] = [1,2,3,4].map(index => { }); type TabWithTriggerProps = React.PropsWithChildren> & { - options: DefaultTabOption[], - defaultActiveTabKey: string, + options?: undefined | Array, + defaultActiveTabKey?: undefined | string, }; const TabWithTrigger = (props: TabWithTriggerProps) => { const { options = defaultTabOptions, defaultActiveTabKey, ...tabContext } = props; - const [activeTabKey, setActiveTabKey] = React.useState(defaultActiveTabKey); + + const [activeTabKey, setActiveTabKey] = React.useState(defaultActiveTabKey); + return ( {options.map(tab => { @@ -58,20 +60,21 @@ const TabWithTrigger = (props: TabWithTriggerProps) => { ); }; +type StoryWithTrigger = StoryObj; -const BaseStory: Story = { +const BaseStory: StoryWithTrigger = { args: {}, render: (args) => , }; -export const Standard: Story = { - ...BaseStory, +export const Standard: StoryWithTrigger = { + ...BaseStory, name: 'Standard', args: { ...BaseStory.args }, }; -export const StandardHover: Story = { - ...BaseStory, +export const StandardHover: StoryWithTrigger = { + ...BaseStory, name: 'Standard [hover]', args: { ...BaseStory.args, @@ -87,8 +90,8 @@ export const StandardHover: Story = { }, }; -export const StandardFocus: Story = { - ...BaseStory, +export const StandardFocus: StoryWithTrigger = { + ...BaseStory, name: 'Standard [focus]', args: { ...BaseStory.args, diff --git a/src/components/navigations/Tabs/Tabs.tsx b/src/components/navigations/Tabs/Tabs.tsx index 8cc59a24..64321e64 100644 --- a/src/components/navigations/Tabs/Tabs.tsx +++ b/src/components/navigations/Tabs/Tabs.tsx @@ -30,7 +30,7 @@ export type TabsProps = React.PropsWithChildren & { unstyled?: undefined | boolean, /** Active key of tab. */ - activeKey?: string, + activeKey?: undefined | string, /** Callback executed when active tab is changed. */ onSwitch: (tabKey: TabKey) => void, @@ -58,7 +58,7 @@ export const Tabs = (props: TabsProps) => { }; const renderActive = (tab: TabElement) => { - let tabElement; + let tabElement: React.ReactNode; if (typeof tab.props.render === 'function') { tabElement = tab.props.render(); } else { @@ -83,10 +83,11 @@ export const Tabs = (props: TabsProps) => {
    {tabs.map(tab => { @@ -94,13 +95,13 @@ export const Tabs = (props: TabsProps) => { const isActive = tab.props.tabKey === activeKey; return (
  • { onSwitch(tab.props.tabKey); }} + onClick={() => { onSwitch(tab.props.tabKey); }} // FIXME: add a Button and use that instead > {tab.props.title}
  • diff --git a/src/components/overlays/DialogModal/DialogModal.stories.tsx b/src/components/overlays/DialogModal/DialogModal.stories.tsx index 4072478d..2dafc253 100644 --- a/src/components/overlays/DialogModal/DialogModal.stories.tsx +++ b/src/components/overlays/DialogModal/DialogModal.stories.tsx @@ -9,6 +9,7 @@ import { LoremIpsum } from '../../../util/storybook/LoremIpsum.tsx'; import { notify } from '../ToastProvider/ToastProvider.tsx'; import { Button } from '../../actions/Button/Button.tsx'; +import { AccountSelector } from '../../../layouts/AppLayout/Header/AccountSelector.tsx'; import { DialogModal } from './DialogModal.tsx'; @@ -116,6 +117,18 @@ export const DialogModalWithToast: Story = { }, }; +export const DialogModalWithDropdown: Story = { + args: { + title: 'Modal with a dropdown', + children: ( + <> +

    The following dropdown menu should overlay the modal (and not be cut off).

    + + + ), + }, +}; + export const DialogModalUncloseable: Story = { args: { activeDefault: true, diff --git a/src/components/overlays/DropdownMenu/DropdownMenu.module.scss b/src/components/overlays/DropdownMenu/DropdownMenu.module.scss index e3d7a019..19c91689 100644 --- a/src/components/overlays/DropdownMenu/DropdownMenu.module.scss +++ b/src/components/overlays/DropdownMenu/DropdownMenu.module.scss @@ -13,6 +13,8 @@ --bk-dropdown-menu-transition-duration: 150ms; overflow: hidden; + /* stylelint-disable-next-line declaration-property-value-disallowed-list */ + overflow-y: auto; min-width: $bk-dropdown-menu-min-width; max-height: 18rem; @@ -74,7 +76,7 @@ &:hover { background: bk.$theme-dropdown-menu-menu-background-hover; } - &[aria-selected="true"] { + &:has(> [aria-selected="true"]) { background: bk.$theme-dropdown-menu-menu-background-hover; box-shadow: inset 5px 0 bk.$theme-dropdown-menu-menu-tab-default; } @@ -94,9 +96,8 @@ align-items: center; gap: bk.$spacing-3; - .bk-dropdown-menu__item__icon { - font-size: 1.4em; - } + .bk-dropdown-menu__item__icon { font-size: 1.4em; } + .bk-dropdown-menu__item__label { --keep: ; } @include bk.focus-inset; } diff --git a/src/components/overlays/DropdownMenu/DropdownMenu.stories.tsx b/src/components/overlays/DropdownMenu/DropdownMenu.stories.tsx index 02265ee7..ead917c0 100644 --- a/src/components/overlays/DropdownMenu/DropdownMenu.stories.tsx +++ b/src/components/overlays/DropdownMenu/DropdownMenu.stories.tsx @@ -21,6 +21,7 @@ export default { argTypes: { }, args: { + label: 'Test dropdown', }, render: (args) => , } satisfies Meta; diff --git a/src/components/overlays/DropdownMenu/DropdownMenu.tsx b/src/components/overlays/DropdownMenu/DropdownMenu.tsx index 3280c611..fff47c41 100644 --- a/src/components/overlays/DropdownMenu/DropdownMenu.tsx +++ b/src/components/overlays/DropdownMenu/DropdownMenu.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { classNames as cx, type ComponentProps } from '../../../util/componentUtil.ts'; -import { useScroller } from '../../../layouts/util/Scroller.tsx'; import { type IconName, Icon } from '../../graphics/Icon/Icon.tsx'; import { Button } from '../../actions/Button/Button.tsx'; @@ -12,8 +11,12 @@ import { Button } from '../../actions/Button/Button.tsx'; import cl from './DropdownMenu.module.scss'; -export { cl as DropdownMenuClassNames }; +/* +References: +- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role +*/ +export { cl as DropdownMenuClassNames }; export type OptionKey = string; export type OptionDef = { optionKey: OptionKey, label: string }; @@ -56,7 +59,7 @@ export const Action = (props: ActionProps) => { const { itemKey, label, icon, onActivate, ...propsRest } = props; const context = useDropdownMenuContext(); - const { optionProps, selectedOption, selectOption } = context; + const { optionProps, selectedOption } = context; const option: OptionDef = { optionKey: itemKey, label }; const isSelected = selectedOption === itemKey; @@ -64,7 +67,10 @@ export const Action = (props: ActionProps) => { return (
  • )} diff --git a/src/components/tables/DataTable/pagination/PaginationStream.module.scss b/src/components/tables/DataTable/pagination/PaginationStream.module.scss index d5317faa..dcb1e6a5 100644 --- a/src/components/tables/DataTable/pagination/PaginationStream.module.scss +++ b/src/components/tables/DataTable/pagination/PaginationStream.module.scss @@ -30,9 +30,10 @@ padding-right: 0; display: flex; + font-weight: bk.$font-weight-regular; &:global(.nonactive) { - opacity: 0.4; + opacity: 0.6; } &.pager__nav--first { --keep: ; } diff --git a/src/components/tables/DataTable/pagination/PaginationStream.tsx b/src/components/tables/DataTable/pagination/PaginationStream.tsx index 61d531b3..5774405f 100644 --- a/src/components/tables/DataTable/pagination/PaginationStream.tsx +++ b/src/components/tables/DataTable/pagination/PaginationStream.tsx @@ -23,9 +23,10 @@ export const PaginationStreamPager = ({ pageSizeOptions }: PaginationStreamPager return (
    diff --git a/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx b/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx index ce3cfe43..52fc5532 100644 --- a/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx +++ b/src/components/tables/DataTable/plugins/useRowSelectColumn.tsx @@ -19,13 +19,13 @@ export const useRowSelectColumn = (hooks: ReactTable.Hooks) Header: ({ getToggleAllPageRowsSelectedProps }) => { const { checked, onChange } = getToggleAllPageRowsSelectedProps(); return ( - + ); }, Cell: ({ row }: ReactTable.CellProps) => { const { checked, onChange } = row.getToggleRowSelectedProps(); return ( - + ); }, }, diff --git a/src/components/tables/DataTable/table/DataTable.tsx b/src/components/tables/DataTable/table/DataTable.tsx index 03582d1a..67b73afa 100644 --- a/src/components/tables/DataTable/table/DataTable.tsx +++ b/src/components/tables/DataTable/table/DataTable.tsx @@ -107,7 +107,7 @@ export const DataTable = (props: DataTableProps) => { {/* { row.toggleRowSelected(); }} + onChange={() => { row.toggleRowSelected(); }} /> */} {row.cells.map(cell => { diff --git a/src/layouts/AppLayout/Breadcrumbs/Breadcrumbs.tsx b/src/layouts/AppLayout/Breadcrumbs/Breadcrumbs.tsx index 1d46c205..83c17949 100644 --- a/src/layouts/AppLayout/Breadcrumbs/Breadcrumbs.tsx +++ b/src/layouts/AppLayout/Breadcrumbs/Breadcrumbs.tsx @@ -43,6 +43,7 @@ export const Breadcrumbs = (props: BreadcrumbsProps) => { const isLast = index === items.length - 1; return (
  • { return ( diff --git a/src/layouts/AppLayout/Header/Header.tsx b/src/layouts/AppLayout/Header/Header.tsx index 2b30b139..3dbb9046 100644 --- a/src/layouts/AppLayout/Header/Header.tsx +++ b/src/layouts/AppLayout/Header/Header.tsx @@ -18,7 +18,9 @@ export type HeaderProps = React.PropsWithChildren & { export const Header = ({ children, unstyled, ...propsRest }: HeaderProps) => { return (
    ` has an implicit default role of "banner", if it is not inside a `
    ` element + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header + //role="banner" {...propsRest} className={cx( 'bk', diff --git a/src/layouts/AppLayout/Header/SolutionSelector.tsx b/src/layouts/AppLayout/Header/SolutionSelector.tsx index b9605b1b..b4cc707e 100644 --- a/src/layouts/AppLayout/Header/SolutionSelector.tsx +++ b/src/layouts/AppLayout/Header/SolutionSelector.tsx @@ -23,6 +23,7 @@ export const SolutionSelector = (props: SolutionSelectorProps) => { return ( diff --git a/src/layouts/AppLayout/Header/UserMenu.tsx b/src/layouts/AppLayout/Header/UserMenu.tsx index 9ce45bb0..c555c4a5 100644 --- a/src/layouts/AppLayout/Header/UserMenu.tsx +++ b/src/layouts/AppLayout/Header/UserMenu.tsx @@ -38,6 +38,7 @@ export const UserMenu = (props: UserMenuProps) => { return ( diff --git a/src/typography/BodyText/BodyText.stories.tsx b/src/typography/BodyText/BodyText.stories.tsx index 729d1201..8ba95798 100644 --- a/src/typography/BodyText/BodyText.stories.tsx +++ b/src/typography/BodyText/BodyText.stories.tsx @@ -3,11 +3,13 @@ |* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; + import type { Meta, StoryObj } from '@storybook/react'; +import { DummyLink } from '../../util/storybook/StorybookLink.tsx'; import { Button } from '../../components/actions/Button/Button.tsx'; -import { Panel } from '../../components/containers/Panel/Panel.tsx'; import { SegmentedControl } from '../../components/forms/controls/SegmentedControl/SegmentedControl.tsx'; +import { Panel } from '../../components/containers/Panel/Panel.tsx'; import { BodyText } from './BodyText.tsx'; @@ -36,7 +38,7 @@ const SampleBodyText = () => {

    Example of body text

    Example of body text

    - Lorem ipsum dolor sit amet, { event.preventDefault(); }}>consectetur adipiscing elit. Pellentesque eget sem ut neque lobortis pharetra nec vel quam. Etiam sem neque, gravida sed pharetra ut, vehicula quis lectus. Donec ac rhoncus purus. Proin ultricies augue vitae purus feugiat, in ultrices lorem aliquet. Donec eleifend ac dolor a auctor. Cras ac suscipit nibh. Fusce tincidunt iaculis dapibus. Vivamus sit amet neque eu velit tincidunt semper. Donec at magna aliquam mi consectetur imperdiet. Donec pretium placerat quam, in sodales purus porta vitae. Phasellus nisl justo, luctus vel mi vel, sollicitudin euismod neque. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget sem ut neque lobortis pharetra nec vel quam. Etiam sem neque, gravida sed pharetra ut, vehicula quis lectus. Donec ac rhoncus purus. Proin ultricies augue vitae purus feugiat, in ultrices lorem aliquet. Donec eleifend ac dolor a auctor. Cras ac suscipit nibh. Fusce tincidunt iaculis dapibus. Vivamus sit amet neque eu velit tincidunt semper. Donec at magna aliquam mi consectetur imperdiet. Donec pretium placerat quam, in sodales purus porta vitae. Phasellus nisl justo, luctus vel mi vel, sollicitudin euismod neque.

    Duis mollis, justo vel pretium luctus, risus sem eleifend lectus, ac convallis dolor nibh id sapien. Donec vestibulum tellus non rutrum convallis. Aenean venenatis enim in egestas lobortis. Donec mollis elit in turpis imperdiet congue vel at magna. Vestibulum bibendum, ipsum quis lobortis lobortis, lorem libero mollis sapien, sed tempus lacus nibh a nunc. Vivamus sed sem eleifend, rutrum erat eget, ultricies neque. Morbi condimentum dolor vel ipsum consectetur iaculis. Donec ornare diam at orci luctus, sed placerat quam dictum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur condimentum molestie augue. Ut vel nisl a augue ornare volutpat. Phasellus et enim in mi maximus ultricies. Integer dignissim ipsum mauris, id bibendum eros euismod et. diff --git a/src/util/componentUtil.ts b/src/util/componentUtil.ts index e58ec63f..23c650a0 100644 --- a/src/util/componentUtil.ts +++ b/src/util/componentUtil.ts @@ -4,11 +4,21 @@ // Note: use the `dedupe` variant so that the consumer of a component can overwrite classes from the component using // `` -import classNames from 'classnames/dedupe'; -import type { Argument as ClassNameArgument } from 'classnames'; +import classNamesDedupe from 'classnames/dedupe'; +import type { Argument as ClassNameArgument, ArgumentArray } from 'classnames'; -export { classNames, type ClassNameArgument }; +export type { ClassNameArgument }; + +export const classNames = (...args: ArgumentArray): string => { + const className = classNamesDedupe(...args); + + if (import.meta.env.MODE === 'development' && className.split(' ').includes('undefined')) { + console.warn('Found `undefined` in class names list'); + } + + return className; +}; // Version of `React.ComponentPropsWithRef` that supports `classnames` syntax for the `className` attribute. So that // components can take class names not just of type string, but any valid classnames `Argument`, e.g.: diff --git a/src/util/storybook/StorybookLink.tsx b/src/util/storybook/StorybookLink.tsx new file mode 100644 index 00000000..8fdbf78b --- /dev/null +++ b/src/util/storybook/StorybookLink.tsx @@ -0,0 +1,9 @@ +/* Copyright (c) Fortanix, Inc. +|* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of +|* the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; + + +export const DummyLink = (props: React.ComponentProps<'a'>) => + { event.preventDefault(); }} {...props}/>; diff --git a/tsconfig.app.json b/tsconfig.app.json index bb8eb14e..ef4c8dd5 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -58,6 +58,5 @@ "exclude": [ "node_modules", "**/*.spec.ts", - "src/components/navigations/Tabs/Tabs.stories.tsx", // FIXME: need to fix a few type errors ], }