From db2455185ca6ae80704ee0753253acb8f2958393 Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 9 Jul 2026 12:12:45 +0500 Subject: [PATCH 1/5] feat(docs): improve a11y and lazy-load Inkeep search widget Add accessible names to icon-only controls (copy-code, clear-input, search) and card overlay links, mark the feedback thumbs as a labeled group, and make the mobile nav overlay a real button. Defer mounting the ~276 KB Inkeep widget (and its Google Fonts stylesheet) until the user activates search: render a lightweight placeholder trigger that mirrors the widget styling to avoid layout shift, forward Cmd/Ctrl+K to it, and mount the real widget with its controlled modal starting open so the same interaction opens search. --- .../pages/docs_landing/views/ai_builder.py | 2 +- .../pages/docs_landing/views/framework.py | 2 +- .../pages/docs_landing/views/link_item.py | 2 +- .../reflex_docs/templates/docpage/docpage.py | 20 ++- docs/app/reflex_docs/views/inkeep.py | 124 +++++++++++++++++- .../src/reflex_components_code/code.py | 1 + .../shiki_code_block.py | 1 + .../components/base/input.py | 1 + 8 files changed, 139 insertions(+), 14 deletions(-) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index caed0932ec5..c1f44ec534c 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -88,7 +88,7 @@ def card( class_name="flex flex-col gap-2 p-8", ), content, - rx.el.a(href=href, class_name="absolute inset-0"), + rx.el.a(href=href, aria_label=title, class_name="absolute inset-0"), class_name="flex flex-col bg-secondary-1/96 backdrop-blur-[16px] rounded-xl relative cursor-pointer transition-colors overflow-hidden shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_12px_24px_0_rgba(0,0,0,0.08),0_1px_1px_0_rgba(0,0,0,0.01),0_4px_8px_0_rgba(0,0,0,0.03)] dark:shadow-none dark:border dark:border-secondary-4", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/framework.py b/docs/app/reflex_docs/pages/docs_landing/views/framework.py index c857b5edefe..216ae938fab 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/framework.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/framework.py @@ -38,7 +38,7 @@ def docs_item( description, class_name="text-secondary-11 text-sm font-[475]", ), - rx.el.a(to=href, class_name="absolute inset-0"), + rx.el.a(to=href, aria_label=title, class_name="absolute inset-0"), class_name="flex flex-col gap-2 py-8 pr-8 relative group lg:max-w-[21rem] w-full max-lg:text-start hover:bg-[linear-gradient(243deg,var(--secondary-2)_0%,var(--secondary-1)_100%)]", ) diff --git a/docs/app/reflex_docs/pages/docs_landing/views/link_item.py b/docs/app/reflex_docs/pages/docs_landing/views/link_item.py index 6097a3dddd2..c86d93ec734 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/link_item.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/link_item.py @@ -43,7 +43,7 @@ def link_item( description, class_name="text-secondary-11 text-sm font-[475] text-start", ), - rx.el.a(to=href, class_name="absolute inset-0"), + rx.el.a(to=href, aria_label=title, class_name="absolute inset-0"), class_name=ui.cn( "flex flex-col gap-2 pr-8 py-8 group border-r border-b border-secondary-4 relative max-lg:p-6 hover:bg-[linear-gradient(243deg,var(--secondary-2)_0%,var(--secondary-1)_100%)]", "lg:pl-8 pl-6" if has_padding_left else "", diff --git a/docs/app/reflex_docs/templates/docpage/docpage.py b/docs/app/reflex_docs/templates/docpage/docpage.py index 997b4195904..bccb0fae12a 100644 --- a/docs/app/reflex_docs/templates/docpage/docpage.py +++ b/docs/app/reflex_docs/templates/docpage/docpage.py @@ -234,22 +234,26 @@ def feedback_content() -> rx.Component: def feedback_button() -> rx.Component: thumb_cn = "w-full gap-2 px-3 py-0.5 flex flex-row items-center justify-center whitespace-nowrap border cursor-pointer transition-colors font-small" return ui.popover.root( - ui.popover.trigger( - render_=rx.el.div( - feedback_choice_button( + rx.el.div( + ui.popover.trigger( + render_=feedback_choice_button( "Yes", "ThumbsUpIcon", 1, ui.cn("rounded-[20px_0_0_20px] border-r-0", thumb_cn), ), - feedback_choice_button( + ), + ui.popover.trigger( + render_=feedback_choice_button( "No", "ThumbsDownIcon", 0, ui.cn("rounded-[0_20px_20px_0]", thumb_cn), ), - class_name="w-full lg:w-auto items-center flex flex-row", ), + role="group", + aria_label="Was this page helpful?", + class_name="w-full lg:w-auto items-center flex flex-row", ), ui.popover.portal( ui.popover.positioner( @@ -746,8 +750,10 @@ def breadcrumb(path: str, nav_sidebar: rx.Component, doc_content: str | None = N return rx.box( docs_sidebar_drawer( nav_sidebar, - trigger=rx.box( - class_name="absolute inset-0 bg-transparent z-[1] lg:hidden flex", + trigger=rx.el.button( + type="button", + aria_label="Open docs navigation", + class_name="absolute inset-0 bg-transparent z-[1] lg:hidden flex border-none p-0 cursor-pointer", ), ), rx.box( diff --git a/docs/app/reflex_docs/views/inkeep.py b/docs/app/reflex_docs/views/inkeep.py index 72a826518e5..d4b88c96e1b 100644 --- a/docs/app/reflex_docs/views/inkeep.py +++ b/docs/app/reflex_docs/views/inkeep.py @@ -1,8 +1,11 @@ """UI and logic inkeep chat component.""" import reflex as rx +from reflex.event import EventSpec +from reflex.experimental.client_state import ClientStateVar from reflex.utils.imports import ImportVar from reflex.vars import Var +from reflex.vars.base import VarData class InkeepSearchBar(rx.NoSSRComponent): @@ -10,6 +13,97 @@ class InkeepSearchBar(rx.NoSSRComponent): library = "@inkeep/cxkit-react@0.5.115" +_INKEEP_LOADED_STATE = "inkeep_loaded" +_INKEEP_OPEN_STATE = "inkeep_open" +_SEARCH_TRIGGER_ID = "inkeep-search-trigger" + +# Inline copy of assets/icons/search.svg so the placeholder needs no extra request. +_SEARCH_ICON_SVG = ( + '' +) + +# Cmd/Ctrl+K opens search before the widget has been loaded: forward the +# shortcut to the placeholder trigger, which mounts the real widget with its +# modal open. Once the widget is mounted, its own shortcut handler takes over. +_HOTKEY_SCRIPT = f""" +(function () {{ + if (window.__inkeepLazyHotkey) return; + window.__inkeepLazyHotkey = true; + document.addEventListener('keydown', function (event) {{ + if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'k') return; + + var trigger = document.getElementById('{_SEARCH_TRIGGER_ID}'); + if (!trigger) return; + + event.preventDefault(); + trigger.click(); + }}); +}})(); +""" + + +def _controlled_modal_settings(opened: ClientStateVar) -> Var: + """Create controlled modal settings for the Inkeep search bar. + + Args: + opened: Client state var tracking whether the modal is open. + + Returns: + A Var containing the Inkeep props and client state dependencies. + """ + is_open = opened.value + on_open_change = opened.set + return Var( + "{...searchBarProps, modalSettings: " + f"{{isOpen: {is_open!s}, onOpenChange: {on_open_change!s}}}}}" + )._replace( + merge_var_data=VarData.merge( + is_open._get_all_var_data(), + on_open_change._get_all_var_data(), + ) + ) + + +def _search_trigger_placeholder(on_click: EventSpec) -> rx.Component: + """Lightweight stand-in for the Inkeep search bar button. + + Mirrors the widget's trigger styling so mounting the real search bar does + not shift layout, while keeping the ~276 KB Inkeep chunk (and the Google + Fonts stylesheet it injects) off the critical path until first use. + + Args: + on_click: Event handler that mounts the real search widget. + + Returns: + The placeholder button component. + """ + return rx.el.button( + rx.html(_SEARCH_ICON_SVG, class_name="flex shrink-0"), + rx.el.span("Search", class_name="max-xl:hidden"), + rx.el.kbd( + "⌘K", + class_name="max-xl:hidden ml-auto flex items-center justify-center px-1 h-5 rounded bg-secondary-3 text-[0.8125rem] font-[475] leading-5 font-sans", + ), + id=_SEARCH_TRIGGER_ID, + type="button", + aria_label="Search documentation", + on_click=on_click, + class_name=( + "flex flex-row items-center justify-center xl:justify-start gap-2 rounded-lg " + "h-8 min-h-8 w-8 xl:w-40 px-0 xl:px-2 " + "bg-secondary-1 dark:bg-secondary-2 hover:bg-secondary-2 dark:hover:bg-secondary-3 " + "text-secondary-11 text-sm font-medium leading-6 border-none cursor-pointer " + "shadow-[0_-1px_0_0_rgba(0,0,0,0.08)_inset,0_0_0_1px_rgba(0,0,0,0.08)_inset,0_1px_2px_0_rgba(0,0,0,0.02),0_1px_4px_0_rgba(0,0,0,0.02)] " + "dark:shadow-[0_-1px_0_0_rgba(255,255,255,0.06)_inset,0_0_0_1px_rgba(255,255,255,0.04)_inset]" + ), + ) + + class Search(rx.el.Div): def add_imports(self): """Add the imports for the component.""" @@ -135,6 +229,9 @@ def add_hooks(self): forcedColorMode: resolvedColorMode, // options: 'light' or dark' }, theme: { + // The site ships its own fonts; skip the widget's default Google Fonts + // (Inter) stylesheet injection. + disableLoadingDefaultFont: true, // Add inline styles using the recommended approach from the docs styles: [ { @@ -390,11 +487,30 @@ def add_hooks(self): @classmethod def create(cls): - """Create the search component.""" + """Create the search component. + + The Inkeep widget is not mounted until the user interacts with the + search trigger, keeping its large bundle out of the initial page load. + The modal is controlled and starts open, so the interaction that + mounts the widget also opens search. + """ + loaded = ClientStateVar.create( + _INKEEP_LOADED_STATE, default=False, global_ref=False + ) + opened = ClientStateVar.create( + _INKEEP_OPEN_STATE, default=True, global_ref=False + ) return super().create( - InkeepSearchBar.create( - special_props=[Var("{...searchBarProps}")], - ) + rx.cond( + loaded.value, + InkeepSearchBar.create( + special_props=[_controlled_modal_settings(opened)], + ), + _search_trigger_placeholder( + on_click=rx.call_function(loaded.set_value(True)), + ), + ), + rx.el.script(_HOTKEY_SCRIPT), ) diff --git a/packages/reflex-components-code/src/reflex_components_code/code.py b/packages/reflex-components-code/src/reflex_components_code/code.py index a6fec988df6..4cd3db193e2 100644 --- a/packages/reflex-components-code/src/reflex_components_code/code.py +++ b/packages/reflex-components-code/src/reflex_components_code/code.py @@ -466,6 +466,7 @@ def create( if copy_button is not None else Button.create( Icon.create(tag="copy"), + aria_label="Copy code", on_click=set_clipboard(code), style=Style({"position": "absolute", "top": "0.5em", "right": "0"}), ) diff --git a/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py b/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py index f18568d5912..c16c89197ed 100644 --- a/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py +++ b/packages/reflex-components-code/src/reflex_components_code/shiki_code_block.py @@ -785,6 +785,7 @@ def create( if copy_button is not None else Button.create( Icon.create(tag="copy", size=16, color=color("gray", 11)), + aria_label="Copy code", on_click=[ set_clipboard(cls._strip_transformer_triggers(code)), copy_script(), diff --git a/packages/reflex-components-internal/src/reflex_components_internal/components/base/input.py b/packages/reflex-components-internal/src/reflex_components_internal/components/base/input.py index 824836ea4af..79d5b6c9faf 100644 --- a/packages/reflex-components-internal/src/reflex_components_internal/components/base/input.py +++ b/packages/reflex-components-internal/src/reflex_components_internal/components/base/input.py @@ -178,6 +178,7 @@ def _create_clear_button(id: str, clear_events: list[EventHandler]) -> Button: return Button.create( hi("CancelCircleIcon"), type="button", + aria_label="Clear input", on_click=[ set_value(id, ""), set_focus(id).stop_propagation, From b377236df0113e5d30b041e08373bb12f56bb0a7 Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 9 Jul 2026 16:12:17 +0500 Subject: [PATCH 2/5] fix(docs): run Inkeep hotkey/hint logic in a mount effect A React-rendered inline