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: stepper component added #523

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Waishnav
Copy link

@Waishnav Waishnav commented Nov 7, 2024

No description provided.

Copy link

netlify bot commented Nov 7, 2024

Deploy Preview for kobalte ready!

Name Link
🔨 Latest commit
🔍 Latest deploy log https://app.netlify.com/sites/kobalte/deploys/673bbae8938ade42f8530ccc
😎 Deploy Preview https://deploy-preview-523--kobalte.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@Waishnav Waishnav marked this pull request as ready for review November 10, 2024 18:51
Copy link
Member

@jer3m01 jer3m01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

There's a few changes needed around accessibility, mostly using a <Tabs> component internally instead of re-implementing them.

packages/core/src/stepper/stepper-item.tsx Show resolved Hide resolved
packages/core/src/stepper/stepper-prev-trigger.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-separator.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-trigger.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-next-trigger.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-item.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-list.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-root.tsx Outdated Show resolved Hide resolved
packages/core/src/stepper/stepper-content.tsx Outdated Show resolved Hide resolved
@Waishnav
Copy link
Author

Thanks for the PR!

There's a few changes needed around accessibility, mostly using a <Tabs> component internally instead of re-implementing them.

Thanks for the detailed review. I've push the new commit and used the Tabs component internally. could you please go through new changes.

@hngngn
Copy link
Contributor

hngngn commented Nov 16, 2024

The previous button is not functioning correctly. When at step 1, it works as expected. However, when navigating to step 2 and then clicking previous, instead of returning to step 1, it moves to step 3.

@Waishnav
Copy link
Author

The previous button is not functioning correctly. When at step 1, it works as expected. However, when navigating to step 2 and then clicking previous, instead of returning to step 1, it moves to step 3.

its old deployment, I've fixed it in new commits.

const [local, others] = splitProps(props as StepperNextTriggerProps, ["onClick"]);

const onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
context.setStep(context.step() + 1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if you had context.moveForward or context.moveNext instead. It would be more encapsulated and straightforward and could be used more efficiently by end users.

Also consider adding something like context.canMoveForward or context.canMoveNext and call them in context.moveForward or context.moveNext so the logic can't be broken.

In addition, you will be able to directly use onClick={context.moveForward} or onClick={context.moveNext}.

const [local, others] = splitProps(props as StepperPrevTriggerProps, ["onClick"]);

const onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
if (context.step() > 0) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if you had context.moveBackward or context.movePrevious instead. It would be more encapsulated and straightforward and could be used more efficiently by end users.

Also consider adding something like context.canMoveBackward or context.canMovePrevious and call them in context.moveBackward or context.movePrevious so the logic can't be broken.

In addition, you will be able to directly use onClick={context.moveBackward} or onClick={context.movePrevious}.

context.setStep(context.step() + 1);
};

const isDisabled = () => context.isDisabled() || context.isCompleted();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add something like context.canMoveForward or context.canMoveNext, add it here too.

}
};

const isDisabled = () => context.isDisabled() || context.step() === 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add something like context.canMoveBackward or context.canMovePrevious, add it here too.

const isDisabled = () => context.isDisabled() || context.step() === 0;

return (
<Show when={!context.isCompleted()}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this logic be handled directly by end users? What if I still want to see these buttons and view all my steps?

disabled?: boolean;

/** The maximum number of steps in the stepper. */
maxSteps: number;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can steps be added dynamically? If so, the term maxSteps may not be appropriate. I would suggest using terms like stepsCount or length instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you suggesting, to handle this maxSteps count internally instead of having this as prop? or suggesting it to change it's name

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, it would be more effective to manage the maxSteps count internally rather than exposing it as a prop. This approach will minimize the potential for human errors and simplify the component overall. You might consider adding context.register in the onMount and context.unregister in the onCleanup method in the Stepper.Content and maybe Stepper.CompletedContent. These functions can be used to increment or decrement the stepsCount or length prop accordingly.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the term StepperStep more suitable than StepperContent?

Additionally, can you determine if the current step is active within this component? It would also be helpful to listen for an event when the current step becomes active, especially in complex multi-step forms.

import { type ElementOf, type PolymorphicProps } from "../polymorphic";

export interface StepperContentOptions {
index: number;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this index be calculated dynamically? For instance, if I don't need the step triggers at the top and want to have dynamic steps?

To accomplish this, I would use a "register and unregister" pattern to adjust the index dynamically and set it for the tab content.

See for example

Copy link
Author

@Waishnav Waishnav Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, stepper forms are not dynamic and have a fixed number of steps. So, I thought it would be better not to consider this edge case.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your approach is quite reasonable. In JSX, you can place step components anywhere and manually set their indexes.

However, let's consider a scenario where you have a room booking form. A room may or may not have zones (i.e. it's large or not), and these zones can also be booked.

In this case, if you have a room selection step and want to dynamically add the next step based on whether the selected room has zones, would it be more effective to implement a regular step skip (i.e. context.setCurrentIndex(context.currentIndex + 2)) or to dynamically add a new step?

Additionally, how would this look if we use Stepper.Triggers for the steps? How will it be displayed visually? Let's say empty or not completed? In dynamic implementation there are also disadvantages for this: UI can "jump"


const onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
if (context.step() > 0) {
context.setStep(context.step() + 1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
context.setStep(context.step() + 1);
context.setStep(context.step() - 1);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, my bad! I think that was a typo—thanks for catching it!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! That's why I suggested adding functions like moveForward and moveBackward to the context. You'll reduce the number of human typos like this!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding another example without step triggers? I think it will be useful for some people

@zobweyt
Copy link

zobweyt commented Nov 18, 2024

Why are there two components Stepper.Item and Stepper.Trigger? Can't they be combined into one? Stepper.Item also gets confused with Stepper.Content

@zobweyt
Copy link

zobweyt commented Nov 18, 2024

In the form example the "Enter" key is ignored. I think this is an accessibility issue

@zobweyt
Copy link

zobweyt commented Nov 18, 2024

What do you think about introducing something like onBeforeStepChange? This will allow us to handle step validation easier. We could listen to this event inside a specific step component and compare the current index with the step index. This way we can move all the validation logic to each individual step, which will help us eliminate long components. As a result, we will wrap each step component in a separate wrapper. This is very useful in complex forms.

Or you can add events to the step component directly, so you don't need to compare the index each time. This is a cool feature for end users and extensibility.

By using onBeforeStepChange you can cancel navigation programmatically and thus indicate that something is wrong with the current step.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants