Skip to content

Commit

Permalink
chore: Instrument modal for funnels (#2829)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeldowseza authored Oct 17, 2024
1 parent c63735b commit f891e72
Show file tree
Hide file tree
Showing 17 changed files with 659 additions and 55 deletions.
105 changes: 105 additions & 0 deletions pages/funnel-analytics/modal.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { FormEvent, useState } from 'react';

import { Alert, Box, Button, ColumnLayout, Container, FormField, Input, Link, Modal, SpaceBetween } from '~components';
import { setFunnelMetrics } from '~components/internal/analytics';

import { MockedFunnelMetrics } from './mock-funnel';
setFunnelMetrics(MockedFunnelMetrics);

const deleteConsentText = 'confirm';
export default function ModalFunnelPage() {
const [visible, setVisible] = useState(false);

const [deleteInputText, setDeleteInputText] = useState('');
const [additionalInputText, setAdditionalInputText] = useState('');
const inputMatchesConsentText = deleteInputText.toLowerCase() === deleteConsentText;

const onDiscard = () => {
setVisible(false);
};

const onConfirm = () => {
setVisible(false);
};

const handleDeleteSubmit = (event: FormEvent) => {
event.preventDefault();
if (inputMatchesConsentText) {
onConfirm();
}
};

return (
<>
<h1>Modal Funnel</h1>
<Button onClick={() => setVisible(true)}>Open Modal</Button>
{visible && (
<Modal
analyticsMetadata={{
flowType: 'delete',
instanceIdentifier: 'delete-flow',
resourceType: 'instance',
}}
onDismiss={() => setVisible(false)}
visible={true}
footer={
<Box float="right">
<SpaceBetween direction="horizontal" size="xs">
<Button variant="link" onClick={onDiscard}>
Cancel
</Button>
<Button variant="primary" onClick={onConfirm} disabled={!inputMatchesConsentText} data-testid="submit">
Delete
</Button>
</SpaceBetween>
</Box>
}
header="Modal title"
>
<SpaceBetween size="m">
<Box variant="span">
Permanently delete instance{' '}
<Box variant="span" fontWeight="bold">
1234567890
</Box>
? You can’t undo this action.
</Box>

<Alert type="warning" statusIconAriaLabel="Warning">
Proceeding with this action will delete the instance with all its content and can affect related
resources.{' '}
<Link external={true} href="#" ariaLabel="Learn more about resource management, opens in new tab">
Learn more
</Link>
</Alert>

<Box>To avoid accidental deletions, we ask you to provide additional written consent.</Box>
<Container>
<FormField label="Nested input">
<Input
placeholder="Additional nested input"
onChange={event => setAdditionalInputText(event.detail.value)}
value={additionalInputText}
/>
</FormField>
</Container>
<form onSubmit={handleDeleteSubmit}>
<FormField label={`To confirm this deletion, type "${deleteConsentText}".`}>
<ColumnLayout columns={2}>
<Input
placeholder={deleteConsentText}
onChange={event => setDeleteInputText(event.detail.value)}
value={deleteInputText}
ariaRequired={true}
/>
</ColumnLayout>
</FormField>
</form>
</SpaceBetween>
</Modal>
)}
</>
);
}
31 changes: 31 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10092,6 +10092,37 @@ The event detail contains the \`reason\`, which can be any of the following:
"functions": [],
"name": "Modal",
"properties": [
{
"analyticsTag": "",
"description": "Specifies additional analytics-related metadata.
* \`instanceIdentifier\` - A unique string that identifies this component instance in your application.
* \`flowType\` - Identifies the type of flow represented by the component.
* \`resourceType\` - Identifies the type of resource represented by the flow. **Note:** This API is currently experimental.",
"inlineType": {
"name": "ModalProps.AnalyticsMetadata",
"properties": [
{
"name": "flowType",
"optional": true,
"type": "FlowType",
},
{
"name": "instanceIdentifier",
"optional": true,
"type": "string",
},
{
"name": "resourceType",
"optional": true,
"type": "string",
},
],
"type": "object",
},
"name": "analyticsMetadata",
"optional": true,
"type": "ModalProps.AnalyticsMetadata",
},
{
"deprecatedTag": "Custom CSS is not supported. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes).",
"description": "Adds the specified classes to the root element of the component.",
Expand Down
17 changes: 3 additions & 14 deletions src/alert/__tests__/alert-analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,15 @@ import {
AnalyticsFunnelSubStep,
} from '../../../lib/components/internal/analytics/components/analytics-funnel';
import { useFunnel } from '../../../lib/components/internal/analytics/hooks/use-funnel';
import { mockFunnelMetrics } from '../../internal/analytics/__tests__/mocks';
import { mockFunnelMetrics, mockGetBoundingClientRect } from '../../internal/analytics/__tests__/mocks';

mockGetBoundingClientRect();

describe('Alert Analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
mockFunnelMetrics();

// These numbers were chosen at random
jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
width: 300,
height: 200,
x: 30,
y: 50,
left: 30,
top: 50,
bottom: 100,
right: 400,
toJSON: () => '',
});
});

test('sends funnelSubStepError metric when the alert is placed inside a substep', () => {
Expand Down
17 changes: 3 additions & 14 deletions src/form-field/__tests__/form-field-analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,15 @@ import {
} from '../../../lib/components/internal/analytics/components/analytics-funnel';
import { useFunnel } from '../../../lib/components/internal/analytics/hooks/use-funnel';
import { DATA_ATTR_FIELD_ERROR, DATA_ATTR_FIELD_LABEL } from '../../../lib/components/internal/analytics/selectors';
import { mockFunnelMetrics } from '../../internal/analytics/__tests__/mocks';
import { mockFunnelMetrics, mockGetBoundingClientRect } from '../../internal/analytics/__tests__/mocks';

mockGetBoundingClientRect();

describe('FormField Analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
mockFunnelMetrics();

// These numbers were chosen at random
jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
width: 300,
height: 200,
x: 30,
y: 50,
left: 30,
top: 50,
bottom: 100,
right: 400,
toJSON: () => '',
});
});

test('sends funnelSubStepError metric when errorText is present', () => {
Expand Down
32 changes: 28 additions & 4 deletions src/form/__tests__/analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import Modal from '../../../lib/components/modal';
import createWrapper from '../../../lib/components/test-utils/dom';
import { mockFunnelMetrics, mockInnerText } from '../../internal/analytics/__tests__/mocks';

import headerStyles from '../../../lib/components/header/styles.selectors.js';
import modalStyles from '../../../lib/components/modal/styles.selectors.js';

mockInnerText();
Expand Down Expand Up @@ -46,11 +45,12 @@ describe('Form Analytics', () => {
funnelType: 'single-page',
totalFunnelSteps: 1,
optionalStepNumbers: [],
funnelNameSelector: `.${headerStyles['heading-text']}`,
funnelName: 'My funnel',
stepConfiguration: [{ isOptional: false, name: 'My funnel', number: 1 }],
funnelNameSelector: expect.any(String),
funnelVersion: expect.any(String),
componentVersion: expect.any(String),
componentTheme: expect.any(String),
stepConfiguration: [{ isOptional: false, name: 'My funnel', number: 1 }],
})
);

Expand Down Expand Up @@ -276,7 +276,7 @@ describe('Form Analytics', () => {
render(<Form errorText="Error" />);
act(() => void jest.runAllTimers());

expect(FunnelMetrics.funnelError).toBeCalledTimes(1);
expect(FunnelMetrics.funnelError).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelError).toHaveBeenCalledWith(
expect.objectContaining({
funnelInteractionId: expect.any(String),
Expand Down Expand Up @@ -378,4 +378,28 @@ describe('Form Analytics', () => {
const form = createWrapper(container).findForm()!.getElement();
expect(form).toHaveAttribute('data-analytics-funnel-step', '1');
});

test('modals to not create a new funnel', () => {
render(
<Form header="Form Funnel">
<Modal header="Modal Funnel" visible={true} />
</Form>
);

act(() => void jest.runAllTimers());
expect(FunnelMetrics.funnelStart).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelStepStart).toHaveBeenCalledTimes(1);

expect(FunnelMetrics.funnelStart).toHaveBeenCalledWith(
expect.objectContaining({
funnelType: 'single-page',
})
);

expect(FunnelMetrics.funnelStepStart).toHaveBeenCalledWith(
expect.objectContaining({
stepName: 'Form Funnel',
})
);
});
});
7 changes: 4 additions & 3 deletions src/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { FormProps } from './interfaces';
import InternalForm from './internal';

import headerStyles from '../header/styles.css.js';
import formStyles from './styles.css.js';
import analyticsSelectors from './analytics-metadata/styles.css.js';

export { FormProps };

Expand Down Expand Up @@ -80,7 +80,8 @@ export default function Form({ variant = 'full-page', ...props }: FormProps) {
analyticsMetadata
);
const inheritedFunnelNameSelector = useFunnelNameSelector();
const funnelNameSelector = inheritedFunnelNameSelector || `.${headerStyles['heading-text']}`;
const funnelNameSelector =
inheritedFunnelNameSelector || `.${analyticsSelectors.header} .${headerStyles['heading-text']}`;

return (
<AnalyticsFunnel
Expand All @@ -91,7 +92,7 @@ export default function Form({ variant = 'full-page', ...props }: FormProps) {
funnelType="single-page"
optionalStepNumbers={[]}
totalFunnelSteps={1}
funnelNameSelectors={[funnelNameSelector, `.${formStyles.header}`]}
funnelNameSelectors={[funnelNameSelector, `.${analyticsSelectors.header}`]}
>
<AnalyticsFunnelStep
stepIdentifier={analyticsMetadata?.instanceIdentifier}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('Multi-page create', () => {
funnelNameSelector: expect.any(String),
funnelVersion: expect.any(String),
funnelIdentifier: FUNNEL_IDENTIFIER,
funnelName: 'Create Resource',
flowType: 'create',
funnelType: 'multi-page',
resourceType: 'Components',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('Single-page create', () => {
funnelNameSelector: expect.any(String),
funnelVersion: expect.any(String),
funnelIdentifier: FUNNEL_IDENTIFIER,
funnelName: 'Form Header',
flowType: 'create',
funnelType: 'single-page',
resourceType: 'Components',
Expand Down
22 changes: 22 additions & 0 deletions src/internal/analytics/__tests__/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ export function mockInnerText() {
afterEach(() => delete (HTMLElement.prototype as Partial<HTMLElement>).innerText);
}
}

export function mockGetBoundingClientRect() {
beforeEach(() => {
Element.prototype.getBoundingClientRect = jest.fn(() => {
return {
width: 100,
height: 100,
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
toJSON: jest.fn(),
};
});
});

afterEach(() => {
delete (Element.prototype as Partial<Element>).getBoundingClientRect;
});
}
Loading

0 comments on commit f891e72

Please sign in to comment.