Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(atom-steps-modal): initial step feature #627

Merged
merged 9 commits into from
Nov 25, 2024
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,15 @@ export namespace Components {
"value"?: IonTypes.IonSelect['value'];
}
interface AtomStepsModal {
"cancelButtonText"?: string;
"closeOnFinish"?: boolean;
"currentStep": number;
"customInitialStep"?: number;
"disablePrimaryButton"?: boolean;
"disableSecondaryButton"?: boolean;
"isOpen": boolean;
"primaryButtonText"?: string;
"secondaryButtonText"?: string;
"primaryButtonTextsByStep": string;
"secondaryButtonTextsByStep": string;
"steps": number;
"stepsTitles": string;
"trigger"?: string;
Expand Down Expand Up @@ -825,8 +827,10 @@ declare namespace LocalJSX {
"value"?: IonTypes.IonSelect['value'];
}
interface AtomStepsModal {
"cancelButtonText"?: string;
"closeOnFinish"?: boolean;
"currentStep"?: number;
"customInitialStep"?: number;
"disablePrimaryButton"?: boolean;
"disableSecondaryButton"?: boolean;
"isOpen"?: boolean;
Expand All @@ -838,8 +842,8 @@ declare namespace LocalJSX {
"onAtomIsOpenChange"?: (event: AtomStepsModalCustomEvent<any>) => void;
"onAtomNextStep"?: (event: AtomStepsModalCustomEvent<any>) => void;
"onAtomPreviousStep"?: (event: AtomStepsModalCustomEvent<any>) => void;
"primaryButtonText"?: string;
"secondaryButtonText"?: string;
"primaryButtonTextsByStep"?: string;
"secondaryButtonTextsByStep"?: string;
"steps"?: number;
"stepsTitles"?: string;
"trigger"?: string;
Expand Down
108 changes: 98 additions & 10 deletions packages/core/src/components/steps-modal/steps-modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ describe('atom-steps-modal', () => {
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
primary-button-text="Next"
secondary-button-text="Previous"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
>
<div slot="step-1">Step 1 Content</div>
<div slot="step-2">Step 2 Content</div>
Expand All @@ -27,8 +27,8 @@ describe('atom-steps-modal', () => {
it('should render modal with default values', async () => {
expect(page.root).toEqualHtml(`
<atom-steps-modal
primary-button-text="Next"
secondary-button-text="Previous"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
Expand All @@ -39,7 +39,7 @@ describe('atom-steps-modal', () => {
class="atom-steps-modal"
primary-button-text="Next"
progress="0.3333333333333333"
secondary-button-text="Previous"
secondary-button-text="Close"
has-footer=""
has-divider=""
header-title="Step 1"
Expand Down Expand Up @@ -129,8 +129,8 @@ describe('atom-steps-modal', () => {
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
primary-button-text="Next"
secondary-button-text="Previous"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
disable-primary-button
disable-secondary-button
>
Expand All @@ -143,8 +143,8 @@ describe('atom-steps-modal', () => {

expect(page.root).toEqualHtml(`
<atom-steps-modal
primary-button-text="Next"
secondary-button-text="Previous"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
Expand All @@ -157,7 +157,7 @@ describe('atom-steps-modal', () => {
class="atom-steps-modal"
primary-button-text="Next"
progress="0.3333333333333333"
secondary-button-text="Previous"
secondary-button-text="Close"
has-footer=""
has-divider=""
header-title="Step 1"
Expand Down Expand Up @@ -273,6 +273,8 @@ describe('atom-steps-modal', () => {
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
current-step="2"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
>
<div slot="step-0">Step 1 Content</div>
<div slot="step-1">Step 2 Content</div>
Expand All @@ -284,6 +286,7 @@ describe('atom-steps-modal', () => {
page.root?.querySelector('atom-modal')?.getAttribute('header-title')
).toBe('Step 2')
})

it('should close modal when closeOnFinish is set', async () => {
page = await newSpecPage({
components: [AtomStepsModal],
Expand All @@ -294,6 +297,8 @@ describe('atom-steps-modal', () => {
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
close-on-finish
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
>
<div slot="step-1">Step 1 Content</div>
<div slot="step-2">Step 2 Content</div>
Expand All @@ -309,6 +314,7 @@ describe('atom-steps-modal', () => {

expect(page.rootInstance.isOpen).toBe(false)
})

it('should emit atomIsOpenChange when modal is opened or closed', async () => {
const isOpenChangeSpy = jest.fn()

Expand All @@ -332,4 +338,86 @@ describe('atom-steps-modal', () => {
expect(isOpenChangeSpy).toHaveBeenCalledTimes(2)
expect(isOpenChangeSpy.mock.calls[1][0].detail).toBe(false)
})

it('should emit atom cancel the modal when customInitialStep is set and the secondary button is clicked on that step', async () => {
page = await newSpecPage({
components: [AtomStepsModal],
html: `
<atom-button id="open-modal-steps">Open Modal</atom-button>
<atom-steps-modal
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
locked-initial-step="2"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
>
<div slot="step-1">Step 1 Content</div>
<div slot="step-2">Step 2 Content</div>
<div slot="step-3">Step 3 Content</div>
</atom-steps-modal>`,
})
const cancelSpy = jest.fn()

page.root?.addEventListener('atomCancel', cancelSpy)

page.root
?.querySelector('atom-modal')
?.dispatchEvent(new CustomEvent('atomSecondaryClick'))

expect(page.rootInstance.currentStep).toBe(2)
expect(cancelSpy).toHaveBeenCalled()
})

it('should adjust progress when locked-initial-step is set', async () => {
page = await newSpecPage({
components: [AtomStepsModal],
html: `
<atom-button id="open-modal-steps">Open Modal</atom-button>
<atom-steps-modal
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
locked-initial-step="2"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
>
<div slot="step-1">Step 1 Content</div>
<div slot="step-2">Step 2 Content</div>
<div slot="step-3">Step 3 Content</div>
</atom-steps-modal>`,
})

expect(page.rootInstance.progress).toBe(0.5)
})

it('should set locked-initial-step to current step when dismiss is called', async () => {
page = await newSpecPage({
components: [AtomStepsModal],
html: `
<atom-button id="open-modal-steps">Open Modal</atom-button>
<atom-steps-modal
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
locked-initial-step="2"
primary-button-texts-by-step="Next, Next, Finish"
secondary-button-texts-by-step="Close, Close, Previous"
>
<div slot="step-1">Step 1 Content</div>
<div slot="step-2">Step 2 Content</div>
<div slot="step-3">Step 3 Content</div>
</atom-steps-modal>`,
})

const mockEventObject = {
stopImmediatePropagation: jest.fn(),
}

page.rootInstance.handlePrimaryClick()
page.rootInstance.handlePrimaryClick()
page.rootInstance.handleDidDismiss(mockEventObject)

expect(page.rootInstance.currentStep).toBe(2)
})
})
42 changes: 38 additions & 4 deletions packages/core/src/components/steps-modal/steps-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ export class AtomStepsModal {
@Prop() trigger?: string
@Prop() stepsTitles: string
@Prop({ mutable: true }) isOpen = false
@Prop() primaryButtonText?: string
@Prop() secondaryButtonText?: string
@Prop() closeOnFinish?: boolean
@Prop() disablePrimaryButton?: boolean
@Prop() disableSecondaryButton?: boolean
@Prop() lockedInitialStep?: number
@Prop() primaryButtonTextsByStep: string
@Prop() secondaryButtonTextsByStep: string

@Event() atomFinish: EventEmitter
@Event() atomCancel: EventEmitter
Expand All @@ -38,6 +39,8 @@ export class AtomStepsModal {
@Element() el!: HTMLElement

private stepsTitlesArray: string[] = []
private primaryButtonTextsArray: string[] = []
private secondaryButtonTextsArray: string[] = []

componentWillLoad() {
const isInvalidCurrentStep =
Expand All @@ -49,7 +52,13 @@ export class AtomStepsModal {
this.currentStep = 1
}

if (this.lockedInitialStep && this.lockedInitialStep >= 1) {
this.currentStep = this.lockedInitialStep
}

this.stepsTitlesArray = this.stepsTitles.split(',')
this.primaryButtonTextsArray = this.primaryButtonTextsByStep.split(',')
this.secondaryButtonTextsArray = this.secondaryButtonTextsByStep.split(',')
}

private handleStep = (step: number) => {
Expand Down Expand Up @@ -85,6 +94,12 @@ export class AtomStepsModal {
}

private handleSecondaryClick = () => {
if (this.currentStep === this.lockedInitialStep) {
this.atomCancel.emit()

return
}

if (this.currentStep === 1) {
this.atomCancel.emit()

Expand All @@ -105,7 +120,26 @@ export class AtomStepsModal {
e.stopImmediatePropagation()
this.isOpen = false
this.atomDidDismiss.emit(this.currentStep)
this.currentStep = 1
this.currentStep = this.lockedInitialStep ?? 1
}

private get secondaryButtonText() {
return this.secondaryButtonTextsArray[this.currentStep - 1].trim()
}

private get primaryButtonText() {
return this.primaryButtonTextsArray[this.currentStep - 1].trim()
}

private get progress() {
if (!this.lockedInitialStep) return this.currentStep / this.steps

const currentStepAdjustedAsInitial =
this.currentStep - this.lockedInitialStep + 1
const stepsQuantityAdjustedToCustomInitial =
this.steps + 1 - this.lockedInitialStep

return currentStepAdjustedAsInitial / stepsQuantityAdjustedToCustomInitial
}

render() {
Expand All @@ -116,7 +150,7 @@ export class AtomStepsModal {
alert-type=''
primary-button-text={this.primaryButtonText}
secondary-button-text={this.secondaryButtonText}
progress={this.currentStep / this.steps}
progress={this.progress}
has-footer=''
header-title={this.stepsTitlesArray[this.currentStep - 1].trim()}
disable-primary={this.disablePrimaryButton}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ export const ModalStoryArgs = {
category: Category.EVENTS,
},
},
lockedInitialStep: {
control: 'number',
description:
'Specifies the step index at which the modal will start. Users are restricted from navigating to steps before this index. Attempting to go back beyond this step will emit atom cancel event from the modal.',
table: {
category: Category.PROPERTIES,
},
},

step_x: {
name: 'step-x',
description:
Expand All @@ -170,6 +179,7 @@ export const ModalComponentArgs = {
currentStep: 1,
isOpen: false,
closeOnFinish: false,
primaryButtonText: 'Next',
secondaryButtonText: 'Previous',
primaryButtonTextsByStep: 'Continue, Continue, Finish',
secondaryButtonTextsByStep: 'Close, Back, Back',
lockedInitialStep: 1,
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const createModal = (args) => {
<atom-steps-modal
steps="3"
trigger="open-modal-steps"
steps-titles="Step 1, Step 2, Step 3"
steps-titles="${args.stepsTitles}"
current-step="${args.currentStep}"
close-on-finish="${args.closeOnFinish}"
primary-button-text="${args.primaryButtonText}"
secondary-button-text="${args.secondaryButtonText}"
primary-button-texts-by-step="${args.primaryButtonTextsByStep}"
secondary-button-texts-by-step="${args.secondaryButtonTextsByStep}"
is-open="${args.isOpen}"
custom-initial-step="${args.customInitialStep}"
>
<div slot="step-1">Step 1 Content</div>
<div slot="step-2">Step 2 Content</div>
Expand All @@ -45,6 +46,14 @@ export const CurrentStepAlreadySet: StoryObj = {
},
}

export const CustomInitialStep: StoryObj = {
render: (args) => createModal(args),
args: {
...ModalComponentArgs,
customInitialStep: 2,
},
}

export const CloseOnFinish: StoryObj = {
render: (args) => createModal(args),
args: {
Expand Down
Loading
Loading