diff --git a/.changeset/large-cows-explain.md b/.changeset/large-cows-explain.md new file mode 100644 index 0000000000..60624ef506 --- /dev/null +++ b/.changeset/large-cows-explain.md @@ -0,0 +1,8 @@ +--- +"@marigold/docs": patch +"@marigold/components": patch +--- + +[DST-494]: add loading states pattern + +[DST-494]: added prop `mode`to the `` to support inline and full-section loading diff --git a/docs/content/components/form/number-field/number-field.mdx b/docs/content/components/form/number-field/number-field.mdx index 828dd814cf..96136cdfbd 100644 --- a/docs/content/components/form/number-field/number-field.mdx +++ b/docs/content/components/form/number-field/number-field.mdx @@ -282,7 +282,7 @@ the user about the error. The message disappears automatically when all requirem }, { title: 'Forms', - href: '../recipes/forms', + href: '../../recipes/form-recipes', caption: 'Here you can find some recipes for some form components.', icon: ( + + + ), + }, + { + title: 'Section Message', + href: '../../components/content/section-message', + caption: 'Display a short message with important informations.', + icon: ( + + + + ), + }, + { + title: 'Validation', + href: '../../foundations/validation', + caption: 'Learn about how to use form validation with Marigold.', + icon: ( + + + + ), + }, + ]} +/> diff --git a/docs/content/patterns/loading-states/full-section.demo.tsx b/docs/content/patterns/loading-states/full-section.demo.tsx new file mode 100644 index 0000000000..4b7ff84ab3 --- /dev/null +++ b/docs/content/patterns/loading-states/full-section.demo.tsx @@ -0,0 +1,65 @@ +import { FormEvent, useState } from 'react'; +import { + Button, + FieldGroup, + Form, + Headline, + Stack, + TextField, + XLoader, +} from '@marigold/components'; + +export default () => { + const [isLoading, setIsLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + const api = (inputValue: string): Promise => { + return new Promise(resolve => { + setTimeout(() => { + resolve(inputValue); + }, 3000); + }); + }; + + const handleOnSubmit = async (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data: { [bookInput: string]: FormDataEntryValue } = + Object.fromEntries(formData); + + setIsLoading(true); + const res = await api(data['bookInput'].toString()); + setSearchTerm(res); + setIsLoading(false); + }; + + return ( +
+
+ + Book search + + + + + + {searchTerm && `You searched for ${searchTerm}.`} + + +
+ {isLoading && } +
+ ); +}; diff --git a/docs/content/patterns/loading-states/inline-indicator.demo.tsx b/docs/content/patterns/loading-states/inline-indicator.demo.tsx new file mode 100644 index 0000000000..1d06778342 --- /dev/null +++ b/docs/content/patterns/loading-states/inline-indicator.demo.tsx @@ -0,0 +1,74 @@ +import { FormEvent, useState } from 'react'; +import { + Button, + FieldGroup, + Form, + Headline, + Stack, + TextField, + XLoader, +} from '@marigold/components'; + +export default () => { + const [isLoading, setIsLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + const api = (inputValue: string): Promise => { + return new Promise(resolve => { + setTimeout(() => { + resolve(inputValue); + }, 3000); + }); + }; + + const handleOnSubmit = async (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + const data: { [bookInput: string]: FormDataEntryValue } = + Object.fromEntries(formData); + + setIsLoading(true); + const res = await api(data['bookInput'].toString()); + setSearchTerm(res); + setIsLoading(false); + }; + + return ( +
+ + Book search + + + + {isLoading ? ( + + ) : ( + + )} + + {searchTerm && `You searched for ${searchTerm}.`} + + +
+ ); +}; diff --git a/docs/content/patterns/loading-states/loading-states.mdx b/docs/content/patterns/loading-states/loading-states.mdx new file mode 100644 index 0000000000..63368d35b0 --- /dev/null +++ b/docs/content/patterns/loading-states/loading-states.mdx @@ -0,0 +1,178 @@ +--- +title: Loading States +caption: Learn when to use which loading state. +badge: new +--- + + + Note + + This pattern is awaiting validation by Reservix product teams. If your team + has decided to implement loading states, please [notify the Design System + Team](https://reservix.slack.com/archives/C02727BNZ3J) as soon as possible + so that we can observe their implementation. + + + +Loading states provide visual feedback to users indicating that an action they have taken has been received and the system is working on their request. + +Loading states help manage user expectations during times of delayed response, and communicate what actions can be taken during and after the loading process. They are crucial for maintaining user engagement and reducing frustration when there are delays in processing user requests. + +## Key principles + +Loading states should be: + +- **Noticeable.** Users should be able to quickly understand that content is loading and where it will appear. +- **Non-interruptive.** Loading indicators should only appear when users need reassurance that their action has been received, and should only block action when necessary. +- **Timely.** Loading indicators should not appear before the user expects them and should disappear promptly, transitioning smoothly to the next state. +- **Communicative.** Any accompanying text should clearly communicate what the system is doing so the user knows how to respond. + +## When to use + +Use loading states when the system needs more than one second to process a user request, such as when fetching data from a server, submitting a form, or uploading a file. + +## When not to use + +Don’t use loading states for actions that are likely to be processed in less than one second. Because loading indicators are another piece of content that has to be mentally processed by the user, only add them when necessary to reduce user uncertainty during longer processes. + +## Types of loading states + +### Inline indicators + + + +Inline indicators are only displayed on a single component, typically a [button](https://www.marigold-ui.io/components/form/button). Use them when: + +- no or little visible content in the current view will be changed. +- the process has no or little visual result in the current view. +- the updated content will only be visible after a clear transition, like redirecting to a new page, closing a drawer, or closing a dialogue. + +Don’t use them over navigational links. + +#### For loading times under 10 seconds + +At this stage: + +- The button state changes to `disabled`. If other actions on the page could affect successful processing, decide if these should be disabled as well. +- The XLoader animation replaces the button label. +- The mouse cursor style changes to `progress`. + +These effects last for a minimum of 1 second regardless of process duration. + +#### Greater than 10 seconds + +Marigold does not currently have a progress bar to show more granular loading states. If no end is in sight, consider if the user needs an “emergency exit” at this point. If so, use the appropriate [feedback message](https://www.marigold-ui.io/patterns/feedback-messages) so the user understands what’s happening and what they can do next. If research indicates that user frustration peaks before 10 seconds, consider showing a feedback message sooner and [notify the design system team](https://www.marigold-ui.io/introduction/get-in-touch). + +#### Result + +- If the process is successful, continue as normal in the user’s flow. Use feedback messages where appropriate to communicate system status. +- If the process is unsuccessful, display an appropriate error message so the user knows why the process failed and what they can do next. + +### Full-section + + + +Full-section indicators are displayed over large sections of content or the entire page. Use them when: + +- large areas of existing, visible content in the current view are changing. +- large areas of new content are being added. +- the user must be prevented from interacting with the content in the affected container or page. + +#### For loading times under 10 seconds + +At this stage: + +- Disable any actions that must be disabled for successful processing. (Ignore actions underneath the full-section indicator, as these will be inaccessible regardless.) +- The XLoader animation appears within an overlay (created via a [dialog](https://www.marigold-ui.io/components/overlay/dialog)) over the container of the loading content. + - If the context requires additional information, add a label under the XLoader. Use labels sparingly, like at the start or end of important tasks or at particularly confusing junctions. +- If the button used to start the process is visible and located outside the container of the loading content, ensure it follows the process for inline indicators as well. +- The mouse cursor style changes to progress. + +These effects last for a minimum of 1 second regardless of process duration. + +#### Greater than 10 seconds + +Marigold does not currently have a progress bar to show more granular loading states. If no end is in sight, consider if the user needs an “emergency exit” at this point. If so, use the appropriate feedback message so they understand what’s happening and what they can do next. If research indicates that user frustration peaks before 10 seconds, consider showing a feedback message sooner and [notify the design system team](https://www.marigold-ui.io/introduction/get-in-touch). + +#### Results + +- If the process is successful, continue as normal in the user’s flow. Use feedback messages where appropriate to communicate system status. +- If the process is unsuccessful, display an appropriate error message so the user knows why the process failed and what they can do next. + +## Placement and appearance + +The loading indicator should always be placed in the center of its container. + +## User interaction + +- Consider if users should have the option to cancel the action if the loading time is too long. +- Provide an option to retry the action if the loading fails due to network issues or other problems. +- User actions can be afforded via labels (in full-section indicators) or feedback messages. + +## Content guidelines + +### Labels + +Avoid labels with the full-section indicator if possible. If they are needed, make sure that they are no more than a few words and provide useful context, like “Finalizing your event…” instead of “Loading…”. Use labels sparingly, like at the start or end of important tasks or at particularly important moments. + +### Feedback messages + +Feedback messages that appear after the process has returned a result should clearly communicate what’s happened and, if appropriate, what actions the user can take next. Follow the [content guidelines for feedback messages](https://www.marigold-ui.io/patterns/feedback-messages?theme=b2b#content-guidelines). + +## References + +[Loading feedback](https://www.pencilandpaper.io/articles/ux-pattern-analysis-loading-feedback) - Pencil & Paper + +[Progress indicators](https://www.nngroup.com/articles/progress-indicators/) - NNGroup + +[Asynchronous UX](https://gitnation.com/contents/asynchronous-ux) - Toni Petrina, React Advanced Conference 2021 + +## Related + + + + + ), + }, + { + title: 'Forms', + href: '../../recipes/form-recipes', + caption: 'Here you can find some recipes for some form components.', + icon: ( + + + + ), + }, + ]} +/> diff --git a/docs/content/patterns/overview.mdx b/docs/content/patterns/overview.mdx index 40519579a2..59c53d7ee5 100644 --- a/docs/content/patterns/overview.mdx +++ b/docs/content/patterns/overview.mdx @@ -75,5 +75,26 @@ By adopting these patterns, designers can save time and effort, focusing instead ), }, + { + title: 'Loading States', + href: '/patterns/loading-states', + caption: 'Learn when to use which loading state.', + icon: ( + + + + ), + }, ]} /> diff --git a/packages/components/src/XLoader/XLoader.stories.tsx b/packages/components/src/XLoader/XLoader.stories.tsx index f1020fd947..89acddef5f 100644 --- a/packages/components/src/XLoader/XLoader.stories.tsx +++ b/packages/components/src/XLoader/XLoader.stories.tsx @@ -39,3 +39,23 @@ type Story = StoryObj; export const Basic: Story = { render: args => , }; + +export const Fullsize: Story = { + render: args => ( + <> + + Loading cause of fetching data... + + + ), +}; + +export const Inline: Story = { + render: args => ( +
+ + I'm loading data. Please wait... + +
+ ), +}; \ No newline at end of file diff --git a/packages/components/src/XLoader/XLoader.tsx b/packages/components/src/XLoader/XLoader.tsx index 36c3397e93..69237b2c5d 100644 --- a/packages/components/src/XLoader/XLoader.tsx +++ b/packages/components/src/XLoader/XLoader.tsx @@ -1,7 +1,24 @@ import { forwardRef } from 'react'; -import { SVG, SVGProps } from '@marigold/system'; +import { Dialog, Modal, ModalOverlay } from 'react-aria-components'; +import { SVG, SVGProps, useClassNames } from '@marigold/system'; +import { Stack } from '../Stack/Stack'; -export const XLoader = forwardRef((props, ref) => ( +export interface XLoaderProps extends SVGProps { + /** + * Show the loader in `fullsize` and blocks interaction with the site or `ìnline` in a certain area. + * @default undefined + */ + mode?: LoadingModeKeys; +} + +const LoadingModes = { + FullSize: 'fullsize', + Inline: 'inline', +} as const; + +type LoadingModeKeys = (typeof LoadingModes)[keyof typeof LoadingModes]; + +const Loader = forwardRef((props, ref) => ( ((props, ref) => (
)); + +const LoaderFullSize = forwardRef( + ({ children, ...rest }, ref) => { + const className = useClassNames({ + component: 'Underlay', + variant: 'modal', + className: + 'fixed left-0 top-0 z-10 flex h-[--visual-viewport-height] w-screen items-center justify-center bg-gray-950/30 cursor-progress', + }); + + return ( + + + + + + {children} + + + + + ); + } +); + +const LoaderInline = forwardRef( + ({ children, ...rest }, ref) => { + return ( +
+ + + {children} + +
+ ); + } +); + +export const XLoader = forwardRef( + ({ mode, ...rest }, ref) => { + return ( + <> + {mode === LoadingModes.FullSize ? ( + + ) : mode === LoadingModes.Inline ? ( + + ) : ( + + )} + + ); + } +);