Skip to content

Commit 57ed6d5

Browse files
committed
fix(ui,sdk)!: give NavButton real state axes and tighten the trigger contracts
1 parent 4574a1e commit 57ed6d5

17 files changed

Lines changed: 204 additions & 108 deletions

File tree

apps/docs/src/components/ui-examples/nav-button.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ export function NavButtonExample() {
1414
<NavButton variant="round" prefixIcon={<House />}>
1515
Round
1616
</NavButton>
17-
<NavButton variant="plain" prefixIcon={<House />}>
18-
Plain
19-
</NavButton>
17+
<NavButton aria-label="Plain" variant="plain" prefixIcon={<House />} />
2018
<NavButton isSelected prefixIcon={<House />} suffixIcon={<ArrowRight />}>
2119
Selected
2220
</NavButton>

apps/docs/src/content/docs/ui-library/ui-components/menu-trigger-button.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import CssVariablesTable from '../../../../components/api/css-variables-table.as
77
import PropsTable from '../../../../components/api/props-table.astro';
88
import { MenuTriggerButtonExample } from '../../../../components/ui-examples/menu-trigger-button';
99

10-
`MenuTriggerButton` is a fixed-size NavButton composition for menu trigger icons.
10+
`MenuTriggerButton` is an icon-only NavButton composition for menu triggers.
1111
Set `isOpen` from the menu state to render the persistent Pressed state.
1212

1313
<MenuTriggerButtonExample client:visible />
@@ -39,6 +39,6 @@ function Example() {
3939
## CSS variables
4040

4141
It renders a `NavButton`, so its appearance is customized through the
42-
[NavButton variables](/docs/ui-library/ui-components/nav-button/#css-variables).
42+
[NavButton variables](/ui-library/ui-components/nav-button/#css-variables).
4343

4444
<CssVariablesTable slug="menu-trigger-button" />

apps/docs/src/content/docs/ui-library/ui-components/nav-button.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import PropsTable from '../../../../components/api/props-table.astro';
88
import { NavButtonExample } from '../../../../components/ui-examples/nav-button';
99

1010
`NavButton` renders its label from `children` and accepts optional `prefixIcon`
11-
and `suffixIcon` props. Leave `children` empty and provide `prefixIcon` for a
12-
square icon-only button, and use `variant` to select square, round, or
13-
no-background styling.
11+
and `suffixIcon` props. Omit `children` and provide `prefixIcon` for an icon-only
12+
button. Use `variant` to select square or round styling; icon-only buttons also
13+
support the no-background `plain` variant.
1414

1515
The `isSelected` prop renders the persistent Pressed state, including while the
1616
button is hovered. The mouse-down `:active` state uses a separate Active token.

packages/sdk/src/features/app-bar/components/controls/controls.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DotsThreeVertical } from '@phosphor-icons/react';
2-
import { Menu, type MenuItemProps, NavButton } from '@workflowbuilder/ui';
3-
import { useMemo } from 'react';
2+
import { Menu, type MenuItemProps, MenuTriggerButton } from '@workflowbuilder/ui';
3+
import { useMemo, useState } from 'react';
44
import { useTranslation } from 'react-i18next';
55

66
import styles from '../../app-bar.module.css';
@@ -12,6 +12,7 @@ import { ToggleReadyOnlyMode } from '../toggle-read-only-mode/toggle-read-only-m
1212

1313
export function Controls() {
1414
const { t } = useTranslation();
15+
const [isMenuOpen, setIsMenuOpen] = useState(false);
1516

1617
// eslint-disable-next-line react-hooks/exhaustive-deps
1718
const items: MenuItemProps[] = useMemo(() => getControlsDotsItems(), [t]);
@@ -24,12 +25,10 @@ export function Controls() {
2425
</OptionalAppBarControls>
2526
{items.length > 0 && (
2627
<div className={styles['menu-container']}>
27-
<Menu items={items}>
28-
<NavButton
29-
aria-label={t('tooltips.menu')}
30-
tooltip={t('tooltips.menu')}
31-
prefixIcon={<DotsThreeVertical />}
32-
/>
28+
<Menu items={items} open={isMenuOpen} onOpenChange={setIsMenuOpen}>
29+
<MenuTriggerButton aria-label={t('tooltips.menu')} isOpen={isMenuOpen} tooltip={t('tooltips.menu')}>
30+
<DotsThreeVertical />
31+
</MenuTriggerButton>
3332
</Menu>
3433
</div>
3534
)}

packages/sdk/src/features/app-bar/components/project-selection/project-selection.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { MenuItemProps } from '@workflowbuilder/ui';
33
import { describe, expect, it, vi } from 'vitest';
44

55
// Render the menu's `items` inline so we can assert on them without driving the
6-
// real menu popover. The component also imports Input/NavButton, so the
6+
// real menu popover. The component also imports Input/MenuTriggerButton, so the
77
// mock must expose them too.
88
vi.mock('@workflowbuilder/ui', () => ({
99
Menu: ({ items }: { items: MenuItemProps[] }) => (
@@ -15,7 +15,7 @@ vi.mock('@workflowbuilder/ui', () => ({
1515
))}
1616
</ul>
1717
),
18-
NavButton: () => null,
18+
MenuTriggerButton: () => null,
1919
Input: () => null,
2020
}));
2121

packages/sdk/src/features/app-bar/components/project-selection/project-selection.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CaretDown } from '@phosphor-icons/react';
2-
import { Input, Menu, NavButton } from '@workflowbuilder/ui';
2+
import { Input, Menu, MenuTriggerButton } from '@workflowbuilder/ui';
33
import { useMemo, useState } from 'react';
44
import { useTranslation } from 'react-i18next';
55

@@ -39,6 +39,7 @@ function ProjectSelectionComponent({ onDuplicateClick }: ProjectSelectionProps)
3939
const isReadOnlyMode = useStore((store) => store.isReadOnlyMode);
4040
const setDocumentName = useStore((state) => state.setDocumentName);
4141
const [editName, setEditName] = useState<boolean>(false);
42+
const [isMenuOpen, setIsMenuOpen] = useState(false);
4243

4344
const { t } = useTranslation();
4445

@@ -88,12 +89,14 @@ function ProjectSelectionComponent({ onDuplicateClick }: ProjectSelectionProps)
8889
</span>
8990
)}
9091
<div className={styles['menu-container']}>
91-
<Menu items={items}>
92-
<NavButton
92+
<Menu items={items} open={isMenuOpen} onOpenChange={setIsMenuOpen}>
93+
<MenuTriggerButton
9394
aria-label={t('tooltips.pickTheProject')}
95+
isOpen={isMenuOpen}
9496
tooltip={t('tooltips.pickTheProject')}
95-
prefixIcon={<CaretDown />}
96-
/>
97+
>
98+
<CaretDown />
99+
</MenuTriggerButton>
97100
</Menu>
98101
</div>
99102
</div>

packages/sdk/src/features/i18n/components/language-selector/language-selector.spec.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ const i18nState = { language: 'en', resolvedLanguage: 'en', changeLanguage: vi.f
88
// Render the Menu's trigger (children) so the displayed language code is queryable.
99
vi.mock('@workflowbuilder/ui', () => ({
1010
Menu: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
11-
NavButton: ({ children }: { children?: ReactNode }) => <button type="button">{children}</button>,
11+
NavButton: ({ 'aria-label': ariaLabel, children }: { 'aria-label'?: string; children?: ReactNode }) => (
12+
<button type="button" aria-label={ariaLabel}>
13+
{children}
14+
</button>
15+
),
1216
}));
1317

1418
vi.mock('@workflow-builder/icons', () => ({
@@ -29,6 +33,7 @@ describe('LanguageSelector — label reflects the resolved language', () => {
2933
render(<LanguageSelector />);
3034

3135
expect(screen.getByText('PL')).toBeTruthy();
36+
expect(screen.getByRole('button', { name: 'PL - tooltips.changeLanguage' })).toBeTruthy();
3237
expect(screen.queryByText('EN')).toBeNull();
3338
});
3439

packages/sdk/src/features/i18n/components/language-selector/language-selector.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function LanguageSelector() {
2020

2121
const resolvedCode = i18n.resolvedLanguage ?? i18n.language?.split('-')[0];
2222
const currentLanguage = languages.find((lang) => lang.code === resolvedCode) || languages[0];
23+
const visibleCode = currentLanguage.code.toUpperCase();
2324

2425
const languageItems: MenuItemProps[] = useMemo(
2526
() =>
@@ -35,11 +36,11 @@ export function LanguageSelector() {
3536
<>
3637
<Menu items={languageItems} size="small">
3738
<NavButton
38-
aria-label={t('tooltips.changeLanguage')}
39+
aria-label={`${visibleCode} - ${t('tooltips.changeLanguage')}`}
3940
suffixIcon={<CaretDown />}
4041
tooltip={t('tooltips.changeLanguage')}
4142
>
42-
{currentLanguage.code.toUpperCase()}
43+
{visibleCode}
4344
</NavButton>
4445
</Menu>
4546
</>

packages/sdk/src/features/variables/components/variable-text/variable-text.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ function SuggestionsContainer({
105105
renderGroupHeader: (group: VariableSuggestionGroup) => ReactNode;
106106
children: ReactElement;
107107
}) {
108+
const { t } = useTranslation();
108109
const ul = children as ReactElement<{ children?: ReactElement[]; className?: string }>;
109110
const items = ul.props.children;
110111

@@ -149,7 +150,7 @@ function SuggestionsContainer({
149150
<div className={styles['suggestionsHeader']}>
150151
<span className={styles['suggestionsTitle']}>{title}</span>
151152
<NavButton
152-
aria-label="Close"
153+
aria-label={t('common.close')}
153154
onMouseDown={(event: React.MouseEvent) => {
154155
event.stopPropagation();
155156
handleClose();

packages/ui/src/components/button/menu-trigger-button/menu-trigger-button.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { forwardRef } from 'react';
2-
import type { ButtonHTMLAttributes, ReactNode } from 'react';
32

43
import { NavButton } from '../nav-button/nav-button';
4+
import type { NavButtonSize, NavButtonVariant } from '../nav-button/types';
5+
import type { BaseButtonProps, IconNode } from '../types';
56

6-
export type MenuTriggerButtonProps = {
7-
children: ReactNode;
7+
export type MenuTriggerButtonProps = BaseButtonProps & {
8+
children: IconNode;
89
isOpen?: boolean;
9-
} & ButtonHTMLAttributes<HTMLButtonElement>;
10+
/** @default 'm' */
11+
size?: NavButtonSize;
12+
/** @default 'square' */
13+
variant?: NavButtonVariant;
14+
};
1015

1116
export const MenuTriggerButton = forwardRef<HTMLButtonElement, MenuTriggerButtonProps>(
1217
({ children, isOpen = false, ...props }, ref) => (

0 commit comments

Comments
 (0)