-
-
Notifications
You must be signed in to change notification settings - Fork 117
feat: Add KSnackbar component and useKSnackbar composable #1205
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
base: develop
Are you sure you want to change the base?
Changes from all commits
cbe527f
119bb35
ae9443e
815480f
7ef6199
d973dd6
669ca6f
530f3ed
7403f1d
1f0389a
7b06104
acbfd11
dabfc8f
ba109c4
2c999e2
40e2236
d4a6187
662f76a
8f21328
bdbc84a
4ea03c7
40cec4c
3e0dc1e
92b426c
1990b00
7d2288b
c983116
daca2ed
5178d36
e6ab040
c187bf8
3e2c3a8
3caec30
8a4bee5
600068e
b33d254
5161a1e
cbcff2f
3cc2990
7038c63
ade6225
6f1a978
2bc9dd9
a4a0f31
56352bb
9d7bf8e
18cff53
090d612
415ff54
31bce7a
91fccd5
e172d12
55442ca
cee36f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| <template> | ||
|
|
||
| <DocsPageTemplate apiDocs> | ||
| <DocsPageSection | ||
| title="Overview" | ||
| anchor="#overview" | ||
| > | ||
| <p> | ||
| The <code>KSnackbar</code> component provides a globally-managed notification system for | ||
| displaying non-critical messages to users. It supports action buttons, custom timing, focus | ||
| management, and full keyboard accessibility. | ||
| </p> | ||
| <p> | ||
| When multiple snackbars are triggered, new messages automatically replace the current one | ||
| with a smooth transition. For status updates that need to change text without animation, use | ||
| <code>forceReuse</code>. | ||
| </p> | ||
| <ul> | ||
| <li>Global notification state via the <code>useKSnackbar</code> composable</li> | ||
| <li>Optional action button for quick follow-up actions</li> | ||
| <li>Auto-hide with configurable duration (or persistent mode)</li> | ||
| <li>Backdrop mode for higher-priority messages</li> | ||
| <li>Bottom offset support for layouts with bottom navigation</li> | ||
| </ul> | ||
| </DocsPageSection> | ||
|
|
||
| <DocsPageSection | ||
| title="Usage" | ||
| anchor="#usage" | ||
| > | ||
| <p> | ||
| The <code>KSnackbar</code> component serves only as the root-level mount point for | ||
| snackbars. Developers should <strong>not</strong> interact with this component directly to | ||
| show messages. | ||
| </p> | ||
|
|
||
| <h3>Global Setup</h3> | ||
| <p> | ||
| Place a single <code>KSnackbar</code> component in your application's root template (e.g., | ||
| <code>App.vue</code>) and bind it to the state provided by the | ||
| <DocsInternalLink | ||
| text="useKSnackbar" | ||
| href="/useksnackbar" | ||
| /> | ||
| composable. | ||
| </p> | ||
|
|
||
| <!-- eslint-disable --> | ||
| <!-- prettier-ignore --> | ||
| <DocsShowCode language="html"> | ||
| <KSnackbar | ||
| :isOpen="snackbarIsVisible" | ||
| :text="snackbarOptions.text" | ||
| :actionText="snackbarOptions.actionText" | ||
| :bottomOffset="snackbarOptions.bottomOffset" | ||
| :backdrop="snackbarOptions.backdrop" | ||
| :autofocus="snackbarOptions.autofocus" | ||
| :autoDismiss="snackbarOptions.autoDismiss" | ||
| :duration="snackbarOptions.duration" | ||
| @action-click="handleActionClick" | ||
| @blur="handleBlur" | ||
| @close="clearSnackbar" | ||
| /> | ||
| </DocsShowCode> | ||
| <!-- eslint-enable --> | ||
|
|
||
| <p> | ||
| For interactive examples (Basic, With Action, Persistent, Force Reuse, etc.), please see the | ||
| <DocsInternalLink | ||
| text="useKSnackbar" | ||
| href="/useksnackbar" | ||
| /> | ||
| composable page. | ||
| </p> | ||
| </DocsPageSection> | ||
| </DocsPageTemplate> | ||
|
|
||
| </template> | ||
|
|
||
|
|
||
| <script> | ||
|
|
||
| export default { | ||
| name: 'DocsKSnackbar', | ||
| }; | ||
|
|
||
| </script> | ||
|
|
||
|
|
||
| <style lang="scss" scoped> | ||
|
|
||
| ::v-deep .k-snackbar-wrapper { | ||
| z-index: 100; | ||
| } | ||
|
|
||
| </style> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,288 @@ | ||
| <template> | ||
|
|
||
| <DocsPageTemplate apiDocs> | ||
| <DocsPageSection | ||
| title="Overview" | ||
| anchor="#overview" | ||
| > | ||
| <p> | ||
| A composable that offers the <code>createSnackbar</code>, <code>clearSnackbar</code>, and | ||
| <code>setSnackbarText</code> functions, as well as the reactive | ||
| <code>snackbarIsVisible</code> and <code>snackbarOptions</code> refs. It is used to manage a | ||
| global snackbar state, allowing any component to trigger a snackbar without having to pass | ||
| props deeply. | ||
| </p> | ||
| </DocsPageSection> | ||
|
|
||
| <DocsPageSection | ||
| title="Usage" | ||
| anchor="#usage" | ||
| > | ||
| <p> | ||
| Before using this composable to show messages, ensure you have mounted the root component. | ||
| For instructions, please see the | ||
| <DocsInternalLink | ||
| text="KSnackbar" | ||
| href="/ksnackbar" | ||
| /> | ||
| global setup. | ||
| </p> | ||
|
|
||
| <h3>Examples</h3> | ||
|
|
||
| <h4>Basic snackbar</h4> | ||
| <p>Use the default behavior for short confirmation messages.</p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/Basic.vue" | ||
| exampleId="basic" | ||
| block | ||
| /> | ||
|
|
||
| <h4>Snackbar with action</h4> | ||
| <p> | ||
| Provide <code>actionText</code> and <code>actionCallback</code> when calling | ||
| <code>createSnackbar()</code> | ||
| to enable an immediate action such as Undo. The callback is stored in the composable and | ||
| executed when the user clicks the action button. | ||
| </p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/WithAction.vue" | ||
| exampleId="with-action" | ||
| block | ||
| /> | ||
|
|
||
| <h4>Persistent snackbar</h4> | ||
| <p> | ||
| Set <code>autoDismiss: false</code> in <code>createSnackbar()</code> to disable auto-hide | ||
| for important messages. Alternatively, set <code>duration: 0</code> to achieve the same | ||
| effect. | ||
| </p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/Persistent.vue" | ||
| exampleId="persistent" | ||
| block | ||
| /> | ||
|
|
||
| <h4>Snackbar with bottom offset</h4> | ||
| <p>Use <code>bottomOffset</code> when a bottom navigation bar or fixed footer is present.</p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/WithBottomOffset.vue" | ||
| exampleId="with-bottom-offset" | ||
| block | ||
| /> | ||
|
|
||
| <h4>Update snackbar without transition</h4> | ||
| <p> | ||
| Use <code>forceReuse</code> to update the snackbar text in place without replaying the | ||
| transition animation. Useful for status updates like connection state changes. | ||
| </p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/ForceReuse.vue" | ||
| exampleId="force-reuse" | ||
| block | ||
| /> | ||
|
|
||
| <h4>Snackbar with autofocus</h4> | ||
| <p> | ||
| Set <code>autofocus: true</code> to immediately focus the action button when the snackbar | ||
| appears. Useful for critical actions that need immediate attention. | ||
| </p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/WithAutofocus.vue" | ||
| exampleId="with-autofocus" | ||
| block | ||
| /> | ||
|
|
||
| <h4>Snackbar with onBlur handling</h4> | ||
| <p> | ||
| Provide an <code>onBlur</code> callback to handle advanced focus management scenarios, such | ||
| as auto-dismissing when the user tabs away or clicks elsewhere. | ||
| </p> | ||
| <DocsExample | ||
| loadExample="KSnackbar/WithOnBlur.vue" | ||
| exampleId="with-onblur" | ||
| block | ||
| /> | ||
| </DocsPageSection> | ||
|
|
||
| <DocsPageSection | ||
| title="Parameters" | ||
| anchor="#parameters" | ||
| > | ||
| <p> | ||
| <code>createSnackbar</code> accepts an <code>options</code> object with the following | ||
| properties: | ||
| </p> | ||
| <PropsTable :api="options" /> | ||
| </DocsPageSection> | ||
|
|
||
| <DocsPageSection | ||
| title="Related" | ||
| anchor="#related" | ||
| > | ||
| <ul> | ||
| <li><DocsLibraryLink component="KSnackbar" /> for the snackbar component</li> | ||
| <li> | ||
| <DocsInternalLink | ||
| text="Snackbars" | ||
| href="/snackbars" | ||
| /> | ||
| has design guidelines and usage guidance | ||
| </li> | ||
| </ul> | ||
| </DocsPageSection> | ||
|
|
||
| <!-- Global snackbar instance for all examples on this page --> | ||
| <KSnackbar | ||
| :isOpen="snackbarIsVisible" | ||
| :text="snackbarOptions.text" | ||
| :actionText="snackbarOptions.actionText" | ||
| :bottomOffset="snackbarOptions.bottomOffset" | ||
| :backdrop="snackbarOptions.backdrop" | ||
| :autofocus="snackbarOptions.autofocus" | ||
| :autoDismiss="snackbarOptions.autoDismiss" | ||
| :duration="snackbarOptions.duration" | ||
| @action-click="handleActionClick" | ||
| @blur="handleBlur" | ||
| @close="clearSnackbar" | ||
| /> | ||
| </DocsPageTemplate> | ||
|
|
||
| </template> | ||
|
|
||
|
|
||
| <script> | ||
|
|
||
| import PropsTable from '../common/DocsPageTemplate/jsdocs/PropsTable'; | ||
| import useKSnackbar from '../../lib/composables/useKSnackbar'; | ||
|
|
||
| export default { | ||
| components: { | ||
| PropsTable, | ||
| }, | ||
| setup() { | ||
| const { snackbarIsVisible, snackbarOptions, clearSnackbar } = useKSnackbar(); | ||
|
|
||
| const handleActionClick = () => { | ||
| if (snackbarOptions.value.actionCallback) { | ||
| snackbarOptions.value.actionCallback(); | ||
| } | ||
| clearSnackbar(); | ||
| }; | ||
|
|
||
| const handleBlur = () => { | ||
| if (typeof snackbarOptions.value.onBlur === 'function') { | ||
| snackbarOptions.value.onBlur(); | ||
| } | ||
| }; | ||
|
|
||
| return { | ||
| snackbarIsVisible, | ||
| snackbarOptions, | ||
| clearSnackbar, | ||
| handleActionClick, | ||
| handleBlur, | ||
| }; | ||
| }, | ||
| data() { | ||
| return { | ||
| options: [ | ||
| { | ||
| name: 'text', | ||
| required: true, | ||
| type: { name: 'string' }, | ||
| description: 'The text to display inside the snackbar.', | ||
| }, | ||
| { | ||
| name: 'actionText', | ||
| required: false, | ||
| default: "''", | ||
| type: { name: 'string' }, | ||
| description: 'Optional text for an action button (e.g. "Undo").', | ||
| }, | ||
| { | ||
| name: 'actionCallback', | ||
| required: false, | ||
| default: 'null', | ||
| type: { name: 'function' }, | ||
| description: | ||
| 'Function stored in composable and called when the action button is clicked. Retrieved via @action-click event handler at the app root level.', | ||
| }, | ||
| { | ||
| name: 'duration', | ||
| required: false, | ||
| default: '5000', | ||
| type: { name: 'number' }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocking: The documented default for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to 5000 |
||
| description: 'Time in ms until the snackbar auto-hides. Set to 0 to disable auto-hide.', | ||
| }, | ||
| { | ||
| name: 'autoDismiss', | ||
| required: false, | ||
| default: 'true', | ||
| type: { name: 'boolean' }, | ||
| description: | ||
| 'Whether the snackbar should auto-dismiss after the duration. More semantic than setting duration to 0.', | ||
| }, | ||
| { | ||
| name: 'bottomOffset', | ||
| required: false, | ||
| default: '0', | ||
| type: { name: 'number' }, | ||
| description: | ||
| 'Additional bottom offset in pixels. Useful when a bottom navigation bar is present.', | ||
| }, | ||
| { | ||
| name: 'backdrop', | ||
| required: false, | ||
| default: 'false', | ||
| type: { name: 'boolean' }, | ||
| description: | ||
| 'If true, shows a darkening backdrop behind the snackbar. Also makes the snackbar announce assertively instead of politely for screen readers.', | ||
| }, | ||
| { | ||
| name: 'autofocus', | ||
| required: false, | ||
| default: 'false', | ||
| type: { name: 'boolean' }, | ||
| description: | ||
| 'If true, autofocuses the action button when the snackbar appears. Improves accessibility for critical actions.', | ||
| }, | ||
| { | ||
| name: 'onBlur', | ||
| required: false, | ||
| default: 'null', | ||
| type: { name: 'function' }, | ||
| description: | ||
| 'Blur event handler for when the action button loses focus. Useful for advanced focus management.', | ||
| }, | ||
| { | ||
| name: 'forceReuse', | ||
| required: false, | ||
| default: 'false', | ||
| type: { name: 'boolean' }, | ||
| description: | ||
| 'When true, updates the current snackbar text in place without replaying the transition animation. Useful for status updates like connection state changes.', | ||
| }, | ||
| { | ||
| name: 'hideCallback', | ||
| required: false, | ||
| default: 'null', | ||
| type: { name: 'function' }, | ||
| description: | ||
| 'Function called when the snackbar is hidden or replaced. Useful for cleanup or promise resolution.', | ||
| }, | ||
| ], | ||
| }; | ||
| }, | ||
| }; | ||
|
|
||
| </script> | ||
|
|
||
|
|
||
| <style lang="scss" scoped> | ||
|
|
||
| ::v-deep .k-snackbar-wrapper { | ||
| z-index: 100; | ||
| } | ||
|
|
||
| </style> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a feeling, and @MisRob can confirm, but I feel that the
KSnackbarpage should show the instructions to set up the snackbars on the app, anduseKSnackbarshould show the instructions of all available tools we have for displaying snackbars (e.g. forceReuse, backdrop, and all examples we have right now in the KSnackbar page), since at the end, it is the composable is tool we will use to actually show the snackbars, not KSnackbar; KSnackbar just acts as a required step to enable the usage ofuseKSnackbar.And in the end, I think people would use the
useKSnackbarpage more, since this is what we as developers will use most; we won't interact withKSnackbarmuch after its initial setup.So, I'd say that a good structure would be:
KSnackbar page:
useKSnackbarflagging that it is the composable the responsible for every interaction with the snackbars.useKSnackbar page:
KSnackbarsaying it is required to have it set up before using this composable.Now, on a completely different note (and a question more for @MisRob): Do we really need to expose all current KSnackbar fields? Do we want to leave the option for people to use snackbars in a different way without
useKSnackbar? Because the way it is implemented right now,KSnackbaris a completely independent, abstract component, anduseKSnackbaracts just as a global store for passing the information to a globalKSnackbarthat should be set up on the app, but really,KSnackbarcan be used independently ofuseKSnackbar.The answer for this will depend on how restrictive/flexible we want to be, but I just want to make sure we are following a path consciously, and not just because that's how it was done before. If we keep with this flexible path, though, it'd be great to have some notes saying that people can use it independently if they have more advanced use cases, perhaps? (I can only imagine use cases where we would need to pass a slot to KSnackbar and display, for example, a KIcon next to the text). If not, we can just wire
KSnackbarwithuseKSnackbar, and calluseKSnackbardirectly insideKSnackbar; this way, we wouldn't need to expose these fields onKSnackbar, and its setup would only be calling<KSnackbar />.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes a lot of sense! I'll wait for @MisRob to weigh in, but I'm happy to reorganize once we have a decision.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, thanks a lot, those are good considerations. I saw then note and will follow-up :)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry it took such a long time ~ I needed to check a bit on the current state & wanted to think through it. Thanks again for bringing this @AlexVelezLl, it's excellent points & taking time to chat about it now will save us from future trouble. Let me show a possible direction in a very simplified snippet & let see what you think (I only checked the implementation very high-level) What about something like this?
As far as I'm aware, most commonly we will just use the global snackbar so that should be the default behavior and it'd be good if it'd be simple & straightforward to setup. At the same time, it's always a good idea to design API in a way that will scale well. And it seems that this PR is already fairly well setup for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And @AlexVelezLl, your suggestion around documentation makes sense to me :)
@Prashant-thakur77 I think you can just make sure that basic documentation structure is setup as per Alex's guidance, but (unless you're specifically interested in technical writing :) no need to spend much time on details. I usually do last round on our docs myself so that it's consistent overall & it's easier for me to tweak directly rather than to post many comments. So just working towards some good initial shape for now will do!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also combine the layered composables and the global flag and leave the
useKLocalSnackbarprivate, and we could have:Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main benefit of the two KDS composables you suggest Alex would be that we don't need to set the global one in each app. So perhaps that'd be the best argument for them ;) In addition to what you mentioned. From that point of view, yes sound like a nice option to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AlexVelezLl, I posted my comment ^ before I saw yours :) We were both probably writing at the same time & the page didn't refresh.
Unless you see some strong benefit, I think I would prefer to not go with the
globalflag I originally suggested, compared to the both later options I consider it the weakest one for a number of reasons.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No strong preferences, hehe! 😅 Just wanted to describe another alternative.
Alright, so @Prashant-thakur77, apologies for the delay. We will go in the direction of exposing both
useKSnackbaranduseKLocalSnackbarfrom KDS. I'd say both composables can live under the sameuseKSnackbarfile, to make the maintenance more straightforward, and then, this module will exposeuseKSnackbaras default, to be consistent with other composables exposed from KDS, and we can exposeuseKLocalSnackbaras a named export. This is mostly because the 99.99% of the time, we will be using justuseKSnackbar, anduseKLocalSnackbarwould be rather a special case.For documentation, let's just include everything on
useKSnackbaras we agreed previously here, but let's add a section pointing to the existence ofuseKLocalSnackbarin case a more granular control over KSnackbar is needed (e.g., using a for the snackbar text). Overall, the external API and installation process will still be the same; the only change for consumers is that they can now useuseKLocalSnackbar.If we are missing something, please let us know!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a plan :) Go for it & thanks everyone.