Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 44 additions & 40 deletions src/login-web-app/src/haapi-stepper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ The HAAPI Frontend Library is a set of React components that provides:
- A built-in, full management of HAAPI flows in the frontend with minimal setup:
```tsx
<HaapiStepper>
<HaapiUIStep />
<HaapiStepperStepUI />
</HaapiStepper>
```
- A simple toolbox to fully customize HAAPI flows in the frontend, composed of the [HAAPI Stepper](#haapi-stepper), [HAAPI UI Step](#haapi-ui-step), and [HAAPI UI Components](#haapi-ui-components).
- A simple toolbox to fully customize HAAPI flows in the frontend, composed of the [HAAPI Stepper](#haapi-stepper), [HAAPI UI Step](#haapi-ui-step), and [HAAPI UI Components](#haapi-stepper-ui-components).

## HAAPI Stepper

Expand Down Expand Up @@ -90,46 +90,50 @@ function App() {

### Usage

Because `HaapiStepper` does not have a UI, it can be used to build custom flow user interfaces from scratch, or it can be used in combination with the [HaapiUIStep](#haapi-ui-step) component, which provides a ready-to-use, highly customizable, HAAPI UI solution.
Because `HaapiStepper` does not have a UI, it can be used to build custom flow user interfaces from scratch, or it can be used in combination with the [HaapiStepperStepUI](#haapi-ui-step) component, which provides a ready-to-use, highly customizable, HAAPI UI solution.

Finally, the `HaapiStepper` can be used in combination with the built-in [HAAPI UI Components](#haapi-ui-components), which help create highly customized UIs while relying on some defaults.
Finally, the `HaapiStepper` can be used in combination with the built-in [HAAPI UI Components](#haapi-stepper-ui-components), which help create highly customized UIs while relying on some defaults.

Check out [the HaapiStepper documentation and usage examples](./feature/stepper/HaapiStepper.tsx)

## HAAPI UI Step

The `HaapiUIStep` component provides a seamless way to implement complete HAAPI authentication flow UIs in your application, allowing extensive customization with minimal setup.
The `HaapiStepperStepUI` component provides a seamless way to implement complete HAAPI authentication flow UIs in your application, allowing extensive customization with minimal setup.

### Basic Setup

```tsx
<HaapiStepper>
<HaapiUIStep />
<HaapiStepperStepUI />
</HaapiStepper>
```

### Usage

Because the `HaapiUIStep` handles all possible HAAPI authentication flows with proper user interfaces (UI), it is the fastest and easiest way to get HAAPI up and running in your application. It is also highly customizable and granular, allowing you to customize some aspects while keeping the defaults for the rest.
Because the `HaapiStepperStepUI` handles all possible HAAPI authentication flows with proper user interfaces (UI), it is the fastest and easiest way to get HAAPI up and running in your application. It is also highly customizable and granular, allowing you to customize some aspects while keeping the defaults for the rest.

Check out [the HaapiUIStep documentation and usage examples](./feature/steps/HaapiUIStep.tsx).
Check out [the HaapiStepperStepUI documentation and usage examples](./feature/steps/HaapiStepperStepUI.tsx).



Comment thread
aleixsuau marked this conversation as resolved.
## HAAPI UI Components
## HAAPI Stepper UI Components

The HAAPI Frontend Library provides some common HAAPI UI elements that help create highly customized UIs while relying on some defaults.
The HAAPI Frontend Library provides some common HAAPI Stepper UI elements that help create highly customized UIs while relying on some defaults.

### Naming convention

The HAAPI Stepper UI components are the UI representation of the main HAAPI entities, named with a `UI` suffix: `HaapiStepperStepUI` displays/interacts with `HaapiStepperStep`, `HaapiStepperLinkUI` with `HaapiStepperLink`, and so on. Collection components use the plural form (`HaapiStepperActionsUI`, `HaapiStepperLinksUI`, `HaapiStepperMessagesUI`).

### Usage

Check out documentation and usage examples in the links below:

- [Form](./feature/actions/form/Form.tsx)
* [Selector](./feature/actions/selector/HaapiSelector.tsx)
* [ClientOperation](./feature/actions/client-operation/ClientOperation.tsx)
* [Messages](./ui/messages/Messages.tsx)
* [Links](./ui/links/Links.tsx)
* [Link](./ui/links/Link.tsx)
* [HaapiStepperFormUI](./feature/actions/form/HaapiStepperFormUI.tsx)
* [HaapiStepperSelectorUI](./feature/actions/selector/HaapiStepperSelectorUI.tsx)
* [HaapiStepperClientOperationUI](./feature/actions/client-operation/HaapiStepperClientOperationUI.tsx)
* [HaapiStepperMessagesUI](./ui/messages/HaapiStepperMessagesUI.tsx)
* [HaapiStepperLinksUI](./ui/links/HaapiStepperLinksUI.tsx)
* [HaapiStepperLinkUI](./ui/links/HaapiStepperLinkUI.tsx)

### CSS Customization

Expand All @@ -139,34 +143,34 @@ The HAAPI UI components reference the CSS classes listed below but do not ship a

| Class | Used by | Purpose |
|-------|---------|---------|
| `.haapi-stepper-selector` | `HaapiSelector` | Selector action container |
| `.haapi-stepper-selector` | `HaapiStepperSelectorUI` | Selector action container |
| `.haapi-stepper-authenticator-button` | `HaapiStepperFormSubmitButton` | Authenticator-selector option button (applied automatically when the action carries `authenticatorType`); combine with `.button-<authenticatorType>` (e.g. `.button-google`) to get the per-authenticator icon color |
| `.haapi-stepper-messages` | `Messages` | Messages container |
| `.haapi-stepper-form-field-text-input` | `HaapiStepperTextFormField` | Text input fields |
| `.haapi-stepper-form-field-text-label` | `HaapiStepperTextFormField` | Form field labels |
| `.haapi-stepper-form-field-checkbox-input` | `HaapiStepperCheckboxFormField` | Checkbox inputs |
| `.haapi-stepper-form-field-checkbox-label` | `HaapiStepperCheckboxFormField` | Checkbox-specific labels |
| `.haapi-stepper-form-field-select-input` | `HaapiStepperSelectFormField` | Select inputs |
| `.haapi-stepper-form-field-select-label` | `HaapiStepperSelectFormField` | Select-specific labels |
| `.haapi-stepper-form-field-password-wrapper` | `HaapiStepperPasswordFormField` | Password input container |
| `.haapi-stepper-form-field-password-label` | `HaapiStepperPasswordFormField` | Password label |
| `.haapi-stepper-form-field-password-input` | `HaapiStepperPasswordFormField` | Password input |
| `.haapi-stepper-form-field-password-visibility-toggle` | `HaapiStepperPasswordFormField` | Password visibility toggle button |
| `.haapi-stepper-button` | `HaapiStepperForm` | Primary submit buttons |
| `.haapi-stepper-button-outline` | `HaapiStepperForm` | Outline/cancel buttons |
| `.haapi-stepper-messages` | `HaapiStepperMessagesUI` | Messages container |
| `.haapi-stepper-form-field-text-input` | `HaapiStepperTextFormFieldUI` | Text input fields |
| `.haapi-stepper-form-field-text-label` | `HaapiStepperTextFormFieldUI` | Form field labels |
| `.haapi-stepper-form-field-checkbox-input` | `HaapiStepperCheckboxFormFieldUI` | Checkbox inputs |
| `.haapi-stepper-form-field-checkbox-label` | `HaapiStepperCheckboxFormFieldUI` | Checkbox-specific labels |
| `.haapi-stepper-form-field-select-input` | `HaapiStepperSelectFormFieldUI` | Select inputs |
| `.haapi-stepper-form-field-select-label` | `HaapiStepperSelectFormFieldUI` | Select-specific labels |
| `.haapi-stepper-form-field-password-wrapper` | `HaapiStepperPasswordFormFieldUI` | Password input container |
| `.haapi-stepper-form-field-password-label` | `HaapiStepperPasswordFormFieldUI` | Password label |
| `.haapi-stepper-form-field-password-input` | `HaapiStepperPasswordFormFieldUI` | Password input |
| `.haapi-stepper-form-field-password-visibility-toggle` | `HaapiStepperPasswordFormFieldUI` | Password visibility toggle button |
| `.haapi-stepper-button` | `HaapiStepperFormUI` | Primary submit buttons |
| `.haapi-stepper-button-outline` | `HaapiStepperFormUI` | Outline/cancel buttons |
| `.haapi-stepper-well` | `Well` | Styled content container |
| `.haapi-stepper-links` | `Links` | Links container |
| `.haapi-stepper-link` | `Link` | Link element |
| `.haapi-stepper-link-qr-code` | `Link` | QR code link figure wrapper |
| `.haapi-stepper-link-qr-code-title` | `Link` | QR code link figcaption |
| `.haapi-stepper-link-qr-code-button` | `Link` | QR code link expand button |
| `.haapi-stepper-links` | `HaapiStepperLinksUI` | Links container |
| `.haapi-stepper-link` | `HaapiStepperLinkUI` | Link element |
| `.haapi-stepper-link-qr-code` | `HaapiStepperLinkUI` | QR code link figure wrapper |
| `.haapi-stepper-link-qr-code-title` | `HaapiStepperLinkUI` | QR code link figcaption |
| `.haapi-stepper-link-qr-code-button` | `HaapiStepperLinkUI` | QR code link expand button |
| `.haapi-stepper-link-qr-code-dialog` | `HaapiStepperQrCodeLinkDialog` | Fullscreen QR code dialog |
| `.haapi-stepper-link-qr-code-dialog-image` | `HaapiStepperQrCodeLinkDialog` | Fullscreen QR code dialog image |
| `.haapi-stepper-actions` | `Actions` | Actions container |
| `.haapi-stepper-heading` | `Messages` | Heading messages |
| `.haapi-stepper-userName` | `Messages` | User name display |
| `.haapi-stepper-userCode` | `Messages` | User code display (e.g. recovery codes) |
| `.haapi-stepper-polling-progress` | `ClientOperation` | Remaining polling time indicator (e.g. recovery codes) |
| `.haapi-stepper-actions` | `HaapiStepperActionsUI` | Actions container |
| `.haapi-stepper-heading` | `HaapiStepperMessagesUI` | Heading messages |
| `.haapi-stepper-userName` | `HaapiStepperMessagesUI` | User name display |
| `.haapi-stepper-userCode` | `HaapiStepperMessagesUI` | User code display (e.g. recovery codes) |
| `.haapi-stepper-polling-progress` | `HaapiStepperClientOperationUI` | Remaining polling time indicator (e.g. recovery codes) |
| `.haapi-stepper-error-boundary-fallback` | `DefaultErrorFallback` | Error boundary fallback container |


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { ReactNode } from 'react';
import { HaapiStepperClientOperationAction, HaapiStepperFormAction } from '../../stepper/haapi-stepper.types';

interface ClientOperationProps {
interface HaapiStepperClientOperationUIProps {
action: HaapiStepperClientOperationAction;
onAction: (action: HaapiStepperClientOperationAction | HaapiStepperFormAction) => void;
showBankIdSessionTimeLeft?: boolean;
Expand All @@ -37,20 +37,20 @@ interface ClientOperationProps {
* const { currentStep, nextStep } = useHaapiStepper(); *
* const clientOperationAction = currentStep?.dataHelpers.clientOperationActions?.[0];
*
* return { clientOperationAction && <ClientOperation action={clientOperationAction} onAction={nextStep} /> };
* return { clientOperationAction && <HaapiStepperClientOperationUI action={clientOperationAction} onAction={nextStep} /> };
* }
*
* <HaapiStepper>
* <HaapiComponentExample />
* </HaapiStepper>
* ```
*/
export function ClientOperation({
export function HaapiStepperClientOperationUI({
action,
onAction,
showBankIdSessionTimeLeft = true,
render = defaultRenderClientOperation,
}: ClientOperationProps) {
}: HaapiStepperClientOperationUIProps) {
return render(action, onAction, showBankIdSessionTimeLeft);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function useHaapiStepperForm(): HaapiStepperFormContextValue {
// eslint-disable-next-line @eslint-react/no-use-context -- useContext is preferred here over use() to keep explicit null handling
const context = useContext(HaapiStepperFormContext);
if (!context) {
throw new Error('useHaapiStepperForm must be used within a <HaapiStepperForm>.');
throw new Error('useHaapiStepperForm must be used within a <HaapiStepperFormUI>.');
}
return context;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { render, screen } from '@testing-library/react';

import { HAAPI_FORM_ACTION_KINDS } from '../../../data-access/types/haapi-action.types';
import { HTTP_METHODS } from '../../../data-access/types/haapi-form.types';
import { HaapiStepperForm } from './HaapiStepperForm';
import { HaapiStepperFormSubmitButton } from './HaapiStepperFormSubmitButton';
import { useHaapiStepper } from '../../stepper/HaapiStepperHook';
import { createHaapiStepperApiMock, createMockFormAction } from '../../../util/tests/mocks';
import { HaapiStepperFormUI } from './HaapiStepperFormUI';

describe('HaapiStepperFormSubmitButton', () => {
beforeEach(() => {
Expand All @@ -19,7 +19,7 @@ describe('HaapiStepperFormSubmitButton', () => {
it('uses action.model.actionTitle as the default label', () => {
const action = createNonAuthenticatorFormAction();

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

expect(screen.getByTestId(submitButtonTestId)).toHaveTextContent(nonAuthenticatorActionTitle);
});
Expand All @@ -31,23 +31,23 @@ describe('HaapiStepperFormSubmitButton', () => {
model: { href: '/login', method: HTTP_METHODS.POST, fields: [] },
});

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

expect(screen.getByTestId(submitButtonTestId)).toHaveTextContent(fallbackActionTitle);
});

it('renders no icon for a non-authenticator form action', () => {
const action = createNonAuthenticatorFormAction();

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

expect(screen.getByTestId(submitButtonTestId).querySelector('.icon')).toBeNull();
});

it('applies the default haapi-stepper-button class for a non-authenticator form action', () => {
const action = createNonAuthenticatorFormAction();

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

const button = screen.getByTestId(submitButtonTestId);
expect(button).toHaveClass('haapi-stepper-button');
Expand All @@ -58,7 +58,7 @@ describe('HaapiStepperFormSubmitButton', () => {
it('applies the outline class and no icon for a cancel form action', () => {
const action = createCancelFormAction();

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

const button = screen.getByTestId(submitButtonTestId);
expect(button).toHaveClass('haapi-stepper-button-outline');
Expand All @@ -71,7 +71,7 @@ describe('HaapiStepperFormSubmitButton', () => {
it('renders the authenticator icon and classes', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'google' });

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

const button = screen.getByTestId(submitButtonTestId);
expect(button).toHaveClass('haapi-stepper-authenticator-button');
Expand All @@ -83,7 +83,7 @@ describe('HaapiStepperFormSubmitButton', () => {
it('falls back to the default icon for an unknown authenticator type', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'some-unknown-type' });

render(<HaapiStepperForm action={action} onSubmit={vi.fn()} />);
render(<HaapiStepperFormUI action={action} onSubmit={vi.fn()} />);

const button = screen.getByTestId(submitButtonTestId);
expect(button).toHaveClass('button-some-unknown-type');
Expand All @@ -98,9 +98,9 @@ describe('HaapiStepperFormSubmitButton', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'google' });

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => <HaapiStepperFormSubmitButton label={customLabel} />}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

const button = screen.getByTestId(submitButtonTestId);
Expand All @@ -114,9 +114,9 @@ describe('HaapiStepperFormSubmitButton', () => {
const action = createNonAuthenticatorFormAction();

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => <HaapiStepperFormSubmitButton icon={<span data-testid={customIconTestId} />} />}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

const button = screen.getByTestId(submitButtonTestId);
Expand All @@ -128,9 +128,9 @@ describe('HaapiStepperFormSubmitButton', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'google' });

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => <HaapiStepperFormSubmitButton icon={<span data-testid={customIconTestId} />} />}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

const button = screen.getByTestId(submitButtonTestId);
Expand All @@ -145,13 +145,13 @@ describe('HaapiStepperFormSubmitButton', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'google' });

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => (
<HaapiStepperFormSubmitButton>
<span data-testid={customChildrenTestId}>Custom content</span>
</HaapiStepperFormSubmitButton>
)}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

const button = screen.getByTestId(submitButtonTestId);
Expand All @@ -167,9 +167,9 @@ describe('HaapiStepperFormSubmitButton', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'google' });

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => <HaapiStepperFormSubmitButton className="my-extra-class" />}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

const button = screen.getByTestId(submitButtonTestId);
Expand All @@ -194,9 +194,9 @@ describe('HaapiStepperFormSubmitButton', () => {
}

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => <FocusSubmitOnMount />}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

expect(screen.getByTestId(submitButtonTestId)).toHaveFocus();
Expand All @@ -208,7 +208,7 @@ describe('HaapiStepperFormSubmitButton', () => {
const action = createAuthenticatorSelectorOption({ authenticatorType: 'google' });

render(
<HaapiStepperForm action={action} onSubmit={vi.fn()}>
<HaapiStepperFormUI action={action} onSubmit={vi.fn()}>
{() => (
<HaapiStepperFormSubmitButton
aria-label="Sign in with Google"
Expand All @@ -217,7 +217,7 @@ describe('HaapiStepperFormSubmitButton', () => {
name="submit"
/>
)}
</HaapiStepperForm>
</HaapiStepperFormUI>
);

const button = screen.getByTestId(submitButtonTestId);
Expand Down
Loading
Loading