diff --git a/packages/ripple-ui-core/package.json b/packages/ripple-ui-core/package.json index 943c963365..07b4e68c66 100644 --- a/packages/ripple-ui-core/package.json +++ b/packages/ripple-ui-core/package.json @@ -20,12 +20,18 @@ "exports": { ".": "./dist/rpl-lib.es.js", "./nuxt": "./src/nuxt/index.ts", + "./components": "./src/components.ts", "./vue": "./dist/rpl-vue.es.js", "./webcomponents": "./dist/web-components/rpl-wc.es.js", "./style": "./dist/global.css", "./style/breakpoints": "./src/styles/_breakpoints.css", "./style/components": "./dist/ripple-ui-core.css" }, + "typesVersions": { + "*": { + "components": ["./src/components.ts"] + } + }, "scripts": { "build": "pnpm clean && pnpm build:types && pnpm build:tokens && pnpm build:utils && pnpm build:styles && pnpm build:lib && pnpm build:vue && pnpm build:wc", "build:types": "tsc -p tsconfig.json", diff --git a/packages/ripple-ui-core/src/components.ts b/packages/ripple-ui-core/src/components.ts index 64f8fc70c2..d59c2a812a 100644 --- a/packages/ripple-ui-core/src/components.ts +++ b/packages/ripple-ui-core/src/components.ts @@ -45,6 +45,7 @@ export { default as RplPrimaryCampaign } from './components/campaign-banner/RplP export { default as RplPrimaryNav } from './components/primary-nav/RplPrimaryNav.vue' export { default as RplProfile } from './components/profile/RplProfile.vue' export { default as RplPromoCard } from './components/card/RplPromoCard.vue' +export { default as RplProgress } from './components/progress/RplProgress.vue' export { default as RplRelatedLinks } from './components/related-links/RplRelatedLinks.vue' export { default as RplResultListing } from './components/result-listing/RplResultListing.vue' export { default as RplResultListingItem } from './components/result-listing/RplResultListingItem.vue' diff --git a/packages/ripple-ui-forms/env.d.ts b/packages/ripple-ui-forms/env.d.ts index e457187449..85e1eceabe 100644 --- a/packages/ripple-ui-forms/env.d.ts +++ b/packages/ripple-ui-forms/env.d.ts @@ -7,4 +7,16 @@ declare module '*.vue' { export default component } +declare module '*.svg?component' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} + +declare module '*.svg?skipsvgo' { + import type { DefineComponent } from 'vue' + const component: DefineComponent + export default component +} + declare module '@dpc-sdp/ripple-ui-core/vue' diff --git a/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts b/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts index 72dac79899..f338b23ae5 100644 --- a/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts +++ b/packages/ripple-ui-forms/src/components/RplForm/RplForm.cy.ts @@ -1,32 +1,92 @@ import RplForm from './RplForm.vue' -import { schema } from './fixtures/sample' +import { + multiStepSchema, + multiStepSchemaNoNextButton, + multiStepSchemaNoPrevButton, + multiStepSchemaSubSteps, + schema +} from './fixtures/sample' -describe('', () => { - it('renders', () => { - // see: https://test-utils.vuejs.org/guide/ - cy.mount(RplForm, { - props: { - id: 'test-form', - schema - } +const mountForm = ( + formSchema: unknown, + extraProps: Record = {} +) => { + cy.mount(RplForm, { + props: { id: 'test-form', schema: formSchema, ...extraProps } + }) +} + +// Helpers +const fill = (label: string, val: string) => { + cy.contains('label', label) + .invoke('attr', 'for') + .then((id) => { + cy.get(`#${id}`).type(val) + }) +} +const select = (label: string, val: string) => { + cy.contains('label', label) + .invoke('attr', 'for') + .then((id) => { + cy.get(`#${id}`).click() + cy.get(`#${id}__menu`).contains(val).click() }) +} +const check = (label: string) => cy.contains('label', label).click() +const submit = () => cy.get('button[type="submit"]').click() +const step = () => cy.get('.rpl-form__step:not([hidden])') +const next = (label = 'Next') => step().contains('button', label).click() +const back = (label = 'Back') => step().contains('button', label).click() + +// Assertions +const assertStep = (heading: string, counter: string) => { + step().contains(heading).should('be.visible') + step().contains(counter).should('be.visible') +} +const assertError = (visible: boolean, errors: string[] = []) => { + cy.get('body').then(($body) => { + const selector = + $body.find('.rpl-form--multi-step').length > 0 + ? '.rpl-form__step:not([hidden]) .rpl-form-alert' + : '.rpl-form-alert' + + if (visible) { + cy.get(selector).should('be.visible') + + if (errors.length) { + cy.get(selector).find('li').as('errors') + cy.get('@errors').should('have.length', errors.length) + + errors.forEach((error, index) => + cy.get('@errors').eq(index).should('contain', error) + ) + } + } else { + cy.get(selector).should('not.exist') + } + }) +} + +describe('', () => { + beforeEach(() => { + cy.viewport(960, 680) + }) + + it('renders with all fields enabled', () => { + mountForm(schema) cy.get('[name="name"]').should('not.be.disabled') cy.get('[name="message"]').should('not.be.disabled') - cy.get('[name="colour"]').should('not.have.attr', 'aria-disabled', 'true') + cy.get('[name="colour"]').should('not.be.disabled') cy.get('[name="pet"]').should('not.be.disabled') cy.get('[name="terms"]').should('not.be.disabled') cy.get('button[type="submit"]').should('not.be.disabled') cy.get('button[type="reset"]').should('not.be.disabled') }) - it('form is disabled while submitting', () => { - cy.mount(RplForm, { - props: { - id: 'test-form', - schema, - submissionState: { status: 'submitting' } - } + it('disables all fields while submitting', () => { + mountForm(schema, { + submissionState: { status: 'submitting' } }) cy.get('[name="name"]').should('be.disabled') @@ -37,4 +97,276 @@ describe('', () => { cy.get('button[type="submit"]').should('be.disabled') cy.get('button[type="reset"]').should('be.disabled') }) + + it('display an error message with invalid fields', () => { + mountForm(schema) + + submit() + + assertError(true, [ + 'Name is required', + 'Message is required', + 'Colour is required', + 'Pet is required', + 'Terms is required' + ]) + + fill('Name', 'Taylor') + fill('Message', 'Hello') + select('Colour', 'Green') + + submit() + + assertError(true, ['Pet is required', 'Terms is required']) + + check('Dog') + check('I accept the terms') + + submit() + + assertError(false) + }) + + describe('', () => { + it('validates and navigates between form steps', () => { + mountForm(multiStepSchema) + + assertStep('Step one', 'Step 1 of 3') + + // Attempt to advance to step two + next() + assertError(true, ['First name is required']) + assertStep('Step one', 'Step 1 of 3') + + // Fill in the required field and advance + fill('First name', 'Taylor') + next() + assertError(false) + assertStep('Step two', 'Step 2 of 3') + + // Attempt to advance to the review step + next('Review') + assertError(true, ['Email is required']) + assertStep('Step two', 'Step 2 of 3') + + // Attempt to advance after filling the required field + fill('Email', 'taylor@example.com') + next('Review') + assertError(false) + assertStep('Review', 'Step 3 of 3') + + // Attempt to submit the form + submit() + assertError(true, ['Terms is required']) + assertStep('Review', 'Step 3 of 3') + + // And go back to the start + back() + assertStep('Step two', 'Step 2 of 3') + + back() + assertStep('Step one', 'Step 1 of 3') + }) + + it('renders review step content', () => { + mountForm(multiStepSchema) + + fill('First name', 'Taylor') + next() + + fill('Email', 'taylor@example.com') + next('Review') + + assertStep('Review', 'Step 3 of 3') + + // Step one review + step() + .contains('.rpl-summary-list__title', 'Step one') + .parent() + .as('stepOne') + + cy.get('@stepOne').contains('.rpl-summary-list__label', 'First name') + cy.get('@stepOne').contains('.rpl-summary-list__value', 'Taylor') + cy.get('@stepOne') + .contains('.rpl-summary-list__action a', 'Change') + .should('have.attr', 'href', '#first_name') + + // Step two review + step() + .contains('.rpl-summary-list__title', 'Step two') + .parent() + .as('stepTwo') + + cy.get('@stepTwo').contains('.rpl-summary-list__label', 'Email') + cy.get('@stepTwo').contains( + '.rpl-summary-list__value', + 'taylor@example.com' + ) + cy.get('@stepTwo') + .contains('.rpl-summary-list__action a', 'Change') + .should('have.attr', 'href', '#email') + + // Action link navigates back to step field + cy.get('@stepOne').find('a[href="#first_name"]').click() + cy.focused().should('have.attr', 'name', 'first_name') + }) + + it('steps can excluded from review content', () => { + mountForm(multiStepSchemaSubSteps) + + fill('First name', 'Taylor') + next() + fill('Last name', 'Smith') + next() + fill('Email', 'taylor@example.com') + next() + check('I accept the terms') + next() + + assertStep('Review', 'Step 3 of 3') + + step() + .contains('.rpl-summary-list__title', 'Step one point one') + .should('not.exist') + + step() + .contains('.rpl-summary-list__title', 'Step one point two') + .should('not.exist') + }) + + it('hides previous step button when prevButton is disabled', () => { + mountForm(multiStepSchemaNoPrevButton) + + step().get('.rpl-form__step-prev').should('not.be.visible') + + next() + assertStep('Step two', 'Step 2 of 3') + step().get('.rpl-form__step-prev').should('not.be.visible') + + next('Continue') + step().get('.rpl-form__step-prev').should('be.visible') + step().get('.rpl-form__step-next').should('not.be.visible') + }) + + it('hides next step button when nextButton is disabled', () => { + mountForm(multiStepSchemaNoNextButton) + + step().get('.rpl-form__step-prev').should('not.be.visible') + + next() + assertStep('Step two', 'Step 2 of 3') + step().get('.rpl-form__step-next').should('not.be.visible') + step().contains('button', 'Back').should('be.visible') + }) + + it('allows navigating between steps with sub-steps', () => { + mountForm(multiStepSchemaSubSteps) + + assertStep('Step one', 'Step 1 of 3') + step().get('.rpl-form__step-prev').should('not.be.visible') + + next() + assertError(true) + + fill('First name', 'Taylor') + + // Go to sub-step 1.1 + next() + assertStep('Step one point one', 'Step 1 of 3') + assertError(false) + step().get('.rpl-form__step-prev').should('be.visible') + + next() + assertError(true) + + fill('Last name', 'Smith') + + // Go to sub-step 1.2 + next() + assertStep('Step one point two', 'Step 1 of 3') + assertError(false) + + next() + assertError(true) + + fill('Email', 'tsmith@mail.com') + + // Go to step 2 + next() + assertStep('Step two', 'Step 2 of 3') + assertError(false) + + next() + assertError(true) + assertStep('Step two', 'Step 2 of 3') + + check('I accept the terms') + + // Go to step 3 + next() + assertStep('Review', 'Step 3 of 3') + assertError(false) + + // Navigate back through steps + back() + assertStep('Step two', 'Step 2 of 3') + + // Skips back to step 1 with custom beforeStepChange + back() + assertStep('Step one', 'Step 1 of 3') + step().find('[name="first_name"]').should('be.visible') + }) + + it('progress indicator does not display sub-steps', () => { + mountForm(multiStepSchemaSubSteps) + + cy.get('.rpl-progress__title').contains('Progress') + cy.get('.rpl-progress__subtitle').contains('Step 1 of 3') + cy.get('.rpl-progress-step').as('steps') + + cy.get('@steps').should('have.length', 3) + cy.get('@steps').eq(0).contains('Step one') + cy.get('@steps').eq(1).contains('Step two') + cy.get('@steps').eq(2).contains('Review') + + cy.get('@steps').eq(0).should('have.class', 'rpl-progress-step--active') + cy.get('@steps') + .eq(1) + .should('not.have.class', 'rpl-progress-step--active') + cy.get('@steps') + .eq(2) + .should('not.have.class', 'rpl-progress-step--active') + + fill('First name', 'Taylor') + + // Go to sub-step 1.1 + next() + cy.get('@steps').should('have.length', 3) + cy.get('@steps').contains('.rpl-progress-step--active', 'Step one') + + fill('Last name', 'Smith') + + // Go to sub-step 1.2 + next() + cy.get('@steps').should('have.length', 3) + cy.get('@steps').contains('.rpl-progress-step--active', 'Step one') + + fill('Email', 'taylor@mail.com') + + // Go to step 2 + next() + cy.get('@steps').should('have.length', 3) + cy.get('@steps').contains('.rpl-progress-step--complete', 'Step one') + cy.get('@steps').contains('.rpl-progress-step--active', 'Step two') + + check('I accept the terms') + + // Go to step 3 + next() + + cy.get('@steps').should('have.length', 3) + cy.get('@steps').contains('.rpl-progress-step--complete', 'Step two') + cy.get('@steps').contains('.rpl-progress-step--active', 'Review') + }) + }) }) diff --git a/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue b/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue index 6f3de3c909..e97e7afc06 100644 --- a/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue +++ b/packages/ripple-ui-forms/src/components/RplForm/RplForm.vue @@ -421,7 +421,7 @@ const handleStepChange = async ({ // Get the current steps errors when it's invalid, and we're trying to proceed if (!currentStep.isValid && forwards) { - cachedErrors.value = getErrorMessages(getNode(currentStep.id)) + cachedErrors.value = getErrorMessages(currentStep.node) } if (isStepValid) { diff --git a/packages/ripple-ui-forms/src/components/RplForm/RplFormMultiStep.stories.ts b/packages/ripple-ui-forms/src/components/RplForm/RplFormMultiStep.stories.ts new file mode 100644 index 0000000000..6036dfbb93 --- /dev/null +++ b/packages/ripple-ui-forms/src/components/RplForm/RplFormMultiStep.stories.ts @@ -0,0 +1,263 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import RplForm from './RplForm.vue' +import { handleEligibilityStepChange } from './fixtures/before-step-change' +import '@dpc-sdp/ripple-ui-core/style/components' + +export default { + title: 'Forms/Multi step form', + component: RplForm +} satisfies Meta + +type Story = StoryObj + +export const DefaultStory: Story = { + name: 'Default', + render: (args) => ({ + components: { RplForm }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + id: 'multi-step-form', + schema: [ + { + $step: true, + id: 'eligibility', + key: 'eligibility', + name: 'eligibility', + title: 'Eligibility', + nextButton: 'Next', + beforeStepChange: handleEligibilityStepChange, + schema: [ + { + $formkit: 'RplFormFieldset', + legend: 'Address', + name: 'address', + children: [ + { + $formkit: 'RplFormText', + id: 'forms_address_organization', + name: 'organization', + label: 'Organization', + validation: [['required']], + validationMessages: { + required: 'The message field is required', + matches: 'Please enter between 10 and 50 characters' + }, + value: '' + }, + { + $formkit: 'RplFormText', + id: 'forms_address_given_name', + name: 'given_name', + label: 'Given name', + validation: [], + value: '' + }, + { + $formkit: 'RplFormText', + id: 'forms_address_family_name', + name: 'family_name', + label: 'Family name', + validation: [], + value: '' + }, + { + $formkit: 'RplFormText', + id: 'forms_address_address_line1', + name: 'address_line1', + label: 'Street address', + validation: [], + value: '' + }, + { + $formkit: 'RplFormText', + id: 'forms_address_address_line2', + name: 'address_line2', + label: 'Street address line 2', + validation: [], + value: '' + }, + { + $formkit: 'RplFormText', + id: 'forms_address_locality', + name: 'locality', + label: 'Suburb', + columnClasses: 'rpl-col-12 rpl-col-5-m', + validation: [], + value: '' + }, + { + $formkit: 'RplFormDropdown', + id: 'forms_address_administrative_area', + name: 'administrative_area', + label: 'State', + columnClasses: 'rpl-col-12 rpl-col-5-m', + options: [ + { + id: 'VIC', + value: 'VIC', + label: 'Victoria' + }, + { + id: 'NSW', + value: 'NSW', + label: 'New South Wales' + }, + { + id: 'WA', + value: 'WA', + label: 'Western Australia' + }, + { + id: 'QLD', + value: 'QLD', + label: 'Queensland' + }, + { + id: 'ACT', + value: 'ACT', + label: 'Australian Capital Territory' + }, + { + id: 'NT', + value: 'NT', + label: 'Northern Territory' + }, + { + id: 'SA', + value: 'SA', + label: 'South Australia' + }, + { + id: 'TAS', + value: 'TAS', + label: 'Tasmania' + } + ], + validation: [], + value: '', + pii: false + }, + { + $formkit: 'RplFormText', + id: 'forms_address_postal_code', + name: 'postal_code', + label: 'Postcode', + columnClasses: 'rpl-col-6 rpl-col-3-m', + validation: [], + value: '' + }, + { + $formkit: 'hidden', + id: 'forms_address_country_code', + name: 'country_code', + value: 'AU' + } + ] + } + ] + }, + { + $step: true, + id: 'not-eligible', + key: 'not-eligible', + name: 'not-eligible', + title: 'Not eligible', + nextButton: false, + prevButton: 'Go backwards', + beforeStepChange: handleEligibilityStepChange, + parentStep: 'eligibility', + schema: [ + { + $el: 'div', + attrs: { + class: 'rpl-content' + }, + children: [ + { + $el: 'p', + children: + 'Unfortunately, based on the information you have provided, you are not eligible to apply for this grant.' + } + ] + } + ] + }, + { + $step: true, + id: 'eligible', + key: 'eligible', + name: 'eligible', + title: 'You are eligible', + nextButton: 'Go forwards', + prevButton: 'Go backwards', + beforeStepChange: handleEligibilityStepChange, + parentStep: 'eligibility', + schema: [ + { + $el: 'div', + attrs: { + class: ['rpl-content', 'rpl-u-padding-b-4'] + }, + children: [ + { + $el: 'p', + children: + 'Congratulations! Based on the information you have provided, you are eligible to apply for this grant.' + } + ] + } + ] + }, + { + $step: true, + id: 'review', + key: 'review', + name: 'review', + title: 'Review', + nextButton: 'Submit', + prevButton: 'Go backwards', + beforeStepChange: handleEligibilityStepChange, + schema: [ + { + $formkit: 'RplFormReview', + key: 'review_component' + }, + { + $formkit: 'RplFormCheckbox', + id: 'terms', + name: 'terms', + label: 'Terms', + checkboxLabel: 'I accept the terms', + validation: [['required']] + }, + { + $formkit: 'RplFormActions', + name: 'submit', + variant: 'filled', + label: 'Submit', + id: 'actions', + displayResetButton: true + } + ] + } + ] + } +} diff --git a/packages/ripple-ui-forms/src/components/RplForm/fixtures/before-step-change.ts b/packages/ripple-ui-forms/src/components/RplForm/fixtures/before-step-change.ts new file mode 100644 index 0000000000..f2e841b7af --- /dev/null +++ b/packages/ripple-ui-forms/src/components/RplForm/fixtures/before-step-change.ts @@ -0,0 +1,57 @@ +import type { StepChangeData } from '@formkit/addons' +export const handleEligibilityStepChange = async ({ + currentStep, + targetStep, + delta +}: StepChangeData) => { + // Delta < 0 indicates navigating backwards. + // We want to allow users to navigate back to the eligibility step, but prevent navigating back to the branch steps once they've moved past them. + if (delta < 0) { + const branchSteps = ['eligible', 'not-eligible'] + const isCurrentBranchStep = branchSteps.includes(currentStep.node.name) + const isTargetBranchStep = branchSteps.includes(targetStep.node.name) + + // Allow branch step -> eligibility directly (no interception). + if (isCurrentBranchStep && targetStep.node.name === 'eligibility') { + return true + } + + // Prevent navigating backwards between branch steps. + if (isCurrentBranchStep && isTargetBranchStep) { + currentStep.node.parent?.goTo('eligibility') + return false + } + + // Prevent review from going back to a branch step directly. + if (currentStep.node.name === 'review' && isTargetBranchStep) { + currentStep.node.parent?.goTo('eligibility') + return false + } + + return true + } + + if (currentStep.node.name === 'eligibility') { + const value = currentStep.value ?? {} + const address = (value as any).address ?? {} + const organization = address.organization + + const desiredStep = organization === 'test' ? 'eligible' : 'not-eligible' + + if (desiredStep !== targetStep.node.name) { + currentStep.node.parent?.goTo(desiredStep) + return false + } + } + + if (currentStep.node.name === 'eligible') { + return true + } + + if (currentStep.node.name === 'not-eligible') { + currentStep.node.parent?.goTo('eligibility') + return false + } + + return true +} diff --git a/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts b/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts index bb589f63e1..bd11cc86cd 100644 --- a/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts +++ b/packages/ripple-ui-forms/src/components/RplForm/fixtures/sample.ts @@ -3,13 +3,15 @@ export const schema = [ $formkit: 'RplFormText', name: 'name', label: 'Name', - id: 'name' + id: 'name', + validation: [['required']] }, { $formkit: 'RplFormTextarea', name: 'message', label: 'Message', - id: 'message' + id: 'message', + validation: [['required']] }, { $formkit: 'RplFormDropdown', @@ -27,7 +29,8 @@ export const schema = [ value: 'Blue', label: 'Blue' } - ] + ], + validation: [['required']] }, { $formkit: 'RplFormRadioGroup', @@ -45,14 +48,16 @@ export const schema = [ value: 'cat', label: 'Cat' } - ] + ], + validation: [['required']] }, { $formkit: 'RplFormCheckbox', id: 'terms', name: 'terms', label: 'Terms', - checkboxLabel: 'I accept the terms' + checkboxLabel: 'I accept the terms', + validation: [['required']] }, { $formkit: 'RplFormActions', @@ -63,3 +68,280 @@ export const schema = [ displayResetButton: true } ] + +export const multiStepSchema: any[] = [ + { + $step: true, + id: 'step-one', + key: 'step-one', + name: 'step-one', + title: 'Step one', + nextButton: 'Next', + schema: [ + { + $formkit: 'RplFormText', + id: 'first_name', + name: 'first_name', + label: 'First name', + validation: [['required']], + value: '' + } + ] + }, + { + $step: true, + id: 'step-two', + key: 'step-two', + name: 'step-two', + title: 'Step two', + nextButton: 'Review', + prevButton: 'Back', + schema: [ + { + $formkit: 'RplFormText', + id: 'email', + name: 'email', + label: 'Email', + validation: [['required']], + value: '' + } + ] + }, + { + $step: true, + id: 'review', + key: 'review', + name: 'review', + title: 'Review', + nextButton: 'Submit', + prevButton: 'Back', + schema: [ + { + $formkit: 'RplFormReview', + key: 'review_component' + }, + { + $formkit: 'RplFormCheckbox', + id: 'terms', + name: 'terms', + label: 'Terms', + checkboxLabel: 'I accept the terms', + validation: [['required']] + }, + { + $formkit: 'RplFormActions', + name: 'submit', + variant: 'filled', + label: 'Submit', + id: 'actions', + displayResetButton: true + } + ] + } +] + +export const multiStepSchemaSubSteps: any[] = [ + { + $step: true, + id: 'step-one', + key: 'step-one', + name: 'step-one', + title: 'Step one', + nextButton: 'Next', + schema: [ + { + $formkit: 'RplFormText', + id: 'first_name', + name: 'first_name', + label: 'First name', + validation: [['required']], + value: '' + } + ] + }, + { + $step: true, + parentStep: 'step-one', + inReview: false, + id: 'step-one-point-one', + key: 'step-one-point-one', + name: 'step-one-point-one', + title: 'Step one point one', + nextButton: 'Next', + schema: [ + { + $formkit: 'RplFormText', + id: 'last_name', + name: 'last_name', + label: 'Last name', + validation: [['required']], + value: '' + } + ] + }, + { + $step: true, + parentStep: 'step-one', + inReview: false, + id: 'step-one-point-two', + key: 'step-one-point-two', + name: 'step-one-point-two', + title: 'Step one point two', + nextButton: 'Next', + schema: [ + { + $formkit: 'RplFormEmail', + id: 'email', + name: 'email', + label: 'Email', + validation: [['required']], + value: '' + } + ] + }, + { + $step: true, + id: 'step-two', + key: 'step-two', + name: 'step-two', + title: 'Step two', + nextButton: 'Next', + beforeStepChange: async ({ currentStep, targetStep }) => { + if (targetStep.id === 'step-one-point-two') { + currentStep.node.parent?.goTo('step-one') + } + return true + }, + schema: [ + { + $formkit: 'RplFormCheckbox', + id: 'terms', + name: 'terms', + label: 'Terms', + checkboxLabel: 'I accept the terms', + validation: [['required']] + } + ] + }, + { + $step: true, + id: 'review', + key: 'review', + name: 'review', + title: 'Review', + nextButton: 'Submit', + prevButton: 'Back', + schema: [ + { + $formkit: 'RplFormReview', + key: 'review_component' + } + ] + } +] + +export const multiStepSchemaNoPrevButton: any[] = [ + { + $step: true, + id: 'step-one', + key: 'step-one', + name: 'step-one', + title: 'Step one', + nextButton: 'Next', + schema: [ + { + $formkit: 'RplFormText', + id: 'first_name', + name: 'first_name', + label: 'First name', + value: '' + } + ] + }, + { + $step: true, + id: 'step-two', + key: 'step-two', + name: 'step-two', + title: 'Step two', + nextButton: 'Continue', + prevButton: false, + schema: [ + { + $formkit: 'RplFormText', + id: 'email', + name: 'email', + label: 'Email', + value: '' + } + ] + }, + { + $step: true, + id: 'review', + key: 'review', + name: 'review', + title: 'Review', + nextButton: 'Submit', + prevButton: 'Back', + schema: [ + { + $formkit: 'RplFormReview', + key: 'review_component' + } + ] + } +] + +export const multiStepSchemaNoNextButton: any[] = [ + { + $step: true, + id: 'step-one', + key: 'step-one', + name: 'step-one', + title: 'Step one', + nextButton: 'Next', + schema: [ + { + $formkit: 'RplFormText', + id: 'first_name_no_next', + name: 'first_name_no_next', + label: 'First name', + value: '' + } + ] + }, + { + $step: true, + id: 'step-two', + key: 'step-two', + name: 'step-two', + title: 'Step two', + nextButton: false, + prevButton: 'Back', + schema: [ + { + $formkit: 'RplFormText', + id: 'email_no_next', + name: 'email_no_next', + label: 'Email', + value: '' + } + ] + }, + { + $step: true, + id: 'review', + key: 'review', + name: 'review', + title: 'Review', + nextButton: 'Submit', + prevButton: 'Back', + schema: [ + { + $formkit: 'RplFormReview', + key: 'review_component' + } + ] + } +] diff --git a/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue b/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue index 99f3bd7804..0ac6aa7b77 100644 --- a/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue +++ b/packages/ripple-ui-forms/src/components/RplFormReview/RplFormReview.vue @@ -3,6 +3,7 @@ import { computed, inject } from 'vue' import { FormKitSchemaNode } from '@formkit/core' import { isValid, parse } from 'date-fns' import { formatDate } from '@dpc-sdp/ripple-ui-core' +import { RplSummaryList } from '@dpc-sdp/ripple-ui-core/components' import { IRplFormProvidedState, RplFormKitStepNode } from '../../types' interface Props { @@ -102,7 +103,7 @@ const items = computed(() => { values = form.stepsId ? values[form.stepsId] || values : values const stepItems = (form?.schema || []).filter( - (item) => (item).$step + (item: RplFormKitStepNode) => item.$step && item.inReview !== false ) as RplFormKitStepNode[] const stepItemsProcessed = stepItems.map((step) => { diff --git a/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue b/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue index 50fcbec795..8a092b2f4a 100644 --- a/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue +++ b/packages/ripple-ui-forms/src/components/RplFormStep/RplFormStep.vue @@ -7,9 +7,11 @@ :input-errors="inputErrors" :previous-label="prevButton" :next-label="nextButton" + :before-step-change="beforeStepChange" + @node="setStepNode" >