Skip to content

Conversation

@gadenbuie
Copy link
Collaborator

Ports rstudio/bslib#1246 to Shiny for Python

gadenbuie and others added 30 commits October 28, 2025 14:38
Implements toast(), toast_header(), show_toast(), and hide_toast() functions
to create temporary, non-intrusive notification messages.

Key features:
- Multiple semantic types (success, warning, error, info, etc.)
- Flexible positioning (9 positions: top/middle/bottom × left/center/right)
- Auto-hide with configurable duration or persistent display
- Optional headers with icons and status text
- Programmatic show/hide control
- Full type safety with proper type hints

This implementation:
- Adds shiny/ui/_toast.py with core functionality
- Exports functions from shiny.ui and shiny.express.ui
- Uses existing vendored bslib JavaScript handlers
- Follows py-shiny patterns from notification and modal systems

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
API Examples:
- Created app-core.py demonstrating all toast features
- Created app-express.py with express syntax examples
- Examples cover: basic toasts, types, positions, duration control

Unit Tests (31 tests, all passing):
- Position normalization tests (11 tests)
- Toast constructor tests (7 tests)
- Toast rendering tests (6 tests)
- ToastHeader tests (4 tests)
- Integration tests (3 tests)

All tests validate:
- Position handling (kebab-case, space-separated, lists, case-insensitive)
- Type validation and error alias
- Autohide and closable combinations
- HTML structure and accessibility attributes
- Header rendering with icons and status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Playwright Controller:
- Created Toast controller in shiny/playwright/controller/_toast.py
- Methods for checking visibility, text content, type, position, autohide
- Exports Toast from controller __init__.py

End-to-End Tests (8 tests):
- test_toast_basic: Basic display and auto-hide
- test_toast_types: Success, warning, error type styling
- test_toast_header: Structured header with icons
- test_toast_positions: Top-left and bottom-right positioning
- test_toast_persistent: Non-auto-hiding toasts
- test_toast_hide: Programmatic hide_toast() functionality
- test_toast_multiple: Multiple simultaneous toasts

Test app provides comprehensive coverage of toast features:
- Different types and positions
- Headers with icons
- Persistent toasts
- Programmatic control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
CHANGELOG:
- Added entry under New features for toast notification system
- Describes key capabilities: types, positions, auto-hide, headers, programmatic control

Quartodoc configs:
- Added toast functions to _quartodoc-core.yml under "Display messages"
  - ui.toast, ui.toast_header, ui.show_toast, ui.hide_toast
- Added express functions to _quartodoc-express.yml
  - express.ui.show_toast, express.ui.hide_toast
- Added Toast controller to _quartodoc-testing.yml
  - playwright.controller.Toast

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fixed test_toast_additional_attributes to use proper keyword arguments
instead of unpacking a dict, which was causing type errors.

Also applied isort formatting fix to test_toast.py.

All checks passing:
- Type check: 0 errors
- Unit tests: 31 tests passed
- Formatting: All files formatted correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updated both app-core.py and app-express.py to provide a comprehensive
toast notification demo similar to the R bslib example. Features include:

- Interactive toast builder with full customization options
- Type and position selection
- Duration control with slider
- Header options with Font Awesome icons (via faicons)
- Advanced features (persistent, long duration, no close button)
- Interactive toasts with action buttons
- Multiple toast demonstrations
- Position testing across all 9 positions
- Dynamic content toast with matplotlib plot (core only)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@gadenbuie gadenbuie requested a review from cpsievert November 5, 2025 19:26
@@ -1,5 +1,5 @@
list(
bslib = "rstudio/bslib@main",
bslib = "rstudio/bslib@feat/toasts",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just a reminder before we merge

Suggested change
bslib = "rstudio/bslib@feat/toasts",
bslib = "rstudio/bslib@main",

Comment on lines +213 to +224
toast_obj = ui.toast(
input.body(),
header=header,
icon=body_icon,
type=input.type() if input.type() else None,
duration_s=input.duration() if input.duration() > 0 else None,
position=input.position(),
closable=input.closable(),
)

# Show and store ID
id = ui.show_toast(toast_obj)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you feel like the ui.modal() API should also look like this (i.e., not be context manager based)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ugh, sorry, I was threading this work between other things and forgot that I intended to update those to use use context managers like modal

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No wait, I was right. ui.modal() looks like this currently and is not a context manager. I kind of think it should be a context manager, but we decided in our conversation earlier this week not to take that on quite yet.

Related: #2108

@@ -0,0 +1,585 @@
from __future__ import annotations

__all__ = ("toast", "toast_header", "show_toast", "hide_toast")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe it's more common to place __all__ after imports

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

After a quick scan, we don't have any internal consistency about this, we're about equally likely to put __all__ above or below imports and sometimes we put __all__ at the bottom of the file.

Personally, I'd rather __all__ appear above imports, since it serves as a sort of table of contents for the most important functions in this file and I feel it gets lost below imports. It turns out PEP8 agrees with this:

According to PEP 8, the official Python style guide, __all__ should be placed after the module docstring but before any import statements (except for from __future__ imports, which must come first).

Comment on lines 60 to 64
def _normalize_type(self, type: Optional[str]) -> Optional[str]:
"""Normalize type, handling 'error' -> 'danger' alias."""
if type == "error":
return "danger"
return type
Copy link
Collaborator

Choose a reason for hiding this comment

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

This abstraction doesn't feel worth it to me

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

@gadenbuie gadenbuie Nov 6, 2025

Choose a reason for hiding this comment

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

Actually, I changed my mind and went the other way with this. I moved this into a helper function (a5b5c67):

  1. that follows the pattern of _normalize_toast_position();
  2. is used in Toast and in the toast pytest controller;
  3. can serve as a place for changes in the future if we add validation or other states.

@gadenbuie gadenbuie requested a review from cpsievert November 6, 2025 15:47
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.

3 participants