This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch docs/federate-storybook in repository https://gitbox.apache.org/repos/asf/superset.git
commit 2c06240b657638eeca746cd5911fad19685f90a8 Author: Evan Rusackas <[email protected]> AuthorDate: Tue Jan 27 16:37:03 2026 -0800 docs(components): fix 10 story migrations for Docs build Fix stories that were broken or incomplete in the Docs build: - Menu: add staticProps for items array, examples for vertical/icons - Modal: add triggerProp/onHideProp pattern so modal doesn't auto-open - ModalTrigger: fix staticProps triggerNode (string instead of nested Button) - Popover: add sampleChildren for trigger element, fix content extraction - ProgressBar: inline args/argTypes (remove spread refs), add all controls - Select: restructure to CSF2, add staticProps for options, fix garbled controls - Slider: add descriptions, liveExample, range/marks/vertical examples - Steps: add staticProps for items array, add vertical/status/dot examples - Switch: demonstrate title as hover tooltip, add settings panel example - TableView: add staticProps for columns/data to fix crash, match Storybook data Generator fixes: - Clear options on non-select/radio types (fixes false-positive shorthand detection) StorybookWrapper fixes: - Skip extractChildren when sampleChildren is provided (fixes Popover content extraction) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- docs/developer_portal/components/ui/menu.mdx | 105 +++++- docs/developer_portal/components/ui/modal.mdx | 121 ++++-- .../components/ui/modaltrigger.mdx | 116 ++++-- docs/developer_portal/components/ui/popover.mdx | 107 +++++- .../developer_portal/components/ui/progressbar.mdx | 82 ++++- docs/developer_portal/components/ui/select.mdx | 405 +++++++++++---------- docs/developer_portal/components/ui/slider.mdx | 165 ++++++--- docs/developer_portal/components/ui/steps.mdx | 162 +++++++-- docs/developer_portal/components/ui/switch.mdx | 120 +++++- docs/developer_portal/components/ui/tableview.mdx | 172 +++++++-- docs/scripts/generate-superset-components.mjs | 33 +- docs/src/components/StorybookWrapper.jsx | 45 ++- .../src/components/Menu/Menu.stories.tsx | 97 ++++- .../src/components/Modal/Modal.stories.tsx | 121 +++++- .../ModalTrigger/ModalTrigger.stories.tsx | 119 +++++- .../src/components/Popover/Popover.stories.tsx | 141 +++++-- .../components/ProgressBar/ProgressBar.stories.tsx | 114 ++++-- .../src/components/Select/Select.stories.tsx | 372 ++++++++++++------- .../src/components/Slider/Slider.stories.tsx | 174 ++++++++- .../src/components/Steps/Steps.stories.tsx | 142 +++++++- .../src/components/Switch/Switch.stories.tsx | 119 +++++- .../src/components/TableView/TableView.stories.tsx | 145 +++++++- 22 files changed, 2511 insertions(+), 666 deletions(-) diff --git a/docs/developer_portal/components/ui/menu.mdx b/docs/developer_portal/components/ui/menu.mdx index bf895c42b9e..7dd4baac68d 100644 --- a/docs/developer_portal/components/ui/menu.mdx +++ b/docs/developer_portal/components/ui/menu.mdx @@ -26,24 +26,35 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Menu -The Menu component from Superset's UI library. +Navigation menu component supporting horizontal, vertical, and inline modes. Based on Ant Design Menu with Superset styling. ## Live Example <StoryWithControls component="Menu" props={{ - inlineCollapsed: false, mode: "horizontal", - multiple: false, - selectable: true + selectable: true, + items: [ + { + label: "Dashboards", + key: "dashboards" + }, + { + label: "Charts", + key: "charts" + }, + { + label: "Datasets", + key: "datasets" + }, + { + label: "SQL Lab", + key: "sqllab" + } + ] }} controls={[ - { - name: "inlineCollapsed", - label: "Inline Collapsed", - type: "boolean" - }, { name: "mode", label: "Mode", @@ -52,17 +63,26 @@ The Menu component from Superset's UI library. "horizontal", "vertical", "inline" - ] + ], + description: "Menu display mode: horizontal navbar, vertical sidebar, or inline collapsible." + }, + { + name: "selectable", + label: "Selectable", + type: "boolean", + description: "Whether menu items can be selected." }, { name: "multiple", label: "Multiple", - type: "boolean" + type: "boolean", + description: "Allow multiple items to be selected." }, { - name: "selectable", - label: "Selectable", - type: "boolean" + name: "inlineCollapsed", + label: "Inline Collapsed", + type: "boolean", + description: "Whether the inline menu is collapsed (only applies to inline mode)." } ]} /> @@ -77,6 +97,56 @@ function Demo() { <Menu mode="horizontal" selectable + items={[ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { label: 'SQL Lab', key: 'sqllab' }, + ]} + /> + ); +} +``` + +## Vertical Menu + +```tsx live +function VerticalMenu() { + return ( + <Menu + mode="vertical" + style={{ width: 200 }} + items={[ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { + label: 'Settings', + key: 'settings', + children: [ + { label: 'Profile', key: 'profile' }, + { label: 'Preferences', key: 'preferences' }, + ], + }, + ]} + /> + ); +} +``` + +## Menu with Icons + +```tsx live +function MenuWithIcons() { + return ( + <Menu + mode="horizontal" + items={[ + { label: <><Icons.DashboardOutlined /> Dashboards</>, key: 'dashboards' }, + { label: <><Icons.LineChartOutlined /> Charts</>, key: 'charts' }, + { label: <><Icons.DatabaseOutlined /> Datasets</>, key: 'datasets' }, + { label: <><Icons.ConsoleSqlOutlined /> SQL Lab</>, key: 'sqllab' }, + ]} /> ); } @@ -86,10 +156,9 @@ function Demo() { | Prop | Type | Default | Description | |------|------|---------|-------------| -| `inlineCollapsed` | `boolean` | `false` | - | -| `mode` | `string` | `"horizontal"` | - | -| `multiple` | `boolean` | `false` | - | -| `selectable` | `boolean` | `true` | - | +| `mode` | `string` | `"horizontal"` | Menu display mode: horizontal navbar, vertical sidebar, or inline collapsible. | +| `selectable` | `boolean` | `true` | Whether menu items can be selected. | +| `items` | `any` | `[{"label":"Dashboards","key":"dashboards"},{"label":"Charts","key":"charts"},{"label":"Datasets","key":"datasets"},{"label":"SQL Lab","key":"sqllab"}]` | - | ## Import diff --git a/docs/developer_portal/components/ui/modal.mdx b/docs/developer_portal/components/ui/modal.mdx index b5be3d4a462..67a984dabb2 100644 --- a/docs/developer_portal/components/ui/modal.mdx +++ b/docs/developer_portal/components/ui/modal.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Modal -The Modal component from Superset's UI library. +Modal dialog component for displaying content that requires user attention or interaction. Supports customizable buttons, drag/resize, and confirmation dialogs. ## Live Example @@ -34,9 +34,9 @@ The Modal component from Superset's UI library. component="Modal" props={{ disablePrimaryButton: false, - primaryButtonName: "Danger", - primaryButtonStyle: "danger", - show: true, + primaryButtonName: "Submit", + primaryButtonStyle: "primary", + show: false, title: "I'm a modal!", resizable: false, draggable: false, @@ -46,12 +46,14 @@ The Modal component from Superset's UI library. { name: "disablePrimaryButton", label: "Disable Primary Button", - type: "boolean" + type: "boolean", + description: "Whether the primary button is disabled." }, { name: "primaryButtonName", label: "Primary Button Name", - type: "text" + type: "text", + description: "Text for the primary action button." }, { name: "primaryButtonStyle", @@ -69,30 +71,36 @@ The Modal component from Superset's UI library. { name: "show", label: "Show", - type: "boolean" + type: "boolean", + description: "Whether the modal is visible. Use the \"Try It\" example below for a working demo." }, { name: "title", label: "Title", - type: "text" + type: "text", + description: "Title displayed in the modal header." }, { name: "resizable", label: "Resizable", - type: "boolean" + type: "boolean", + description: "Whether the modal can be resized by dragging corners." }, { name: "draggable", label: "Draggable", - type: "boolean" + type: "boolean", + description: "Whether the modal can be dragged by its header." }, { name: "width", label: "Width", type: "number", - description: "Width of the modal in pixels or as a string." + description: "Width of the modal in pixels." } ]} + triggerProp="show" + onHideProp="onHide" /> ## Try It @@ -100,15 +108,74 @@ The Modal component from Superset's UI library. Edit the code below to experiment with the component: ```tsx live -function Demo() { +function ModalDemo() { + const [isOpen, setIsOpen] = React.useState(false); return ( - <Modal - primaryButtonName="Danger" - primaryButtonStyle="danger" - show - title="I'm a modal!" - width={500} - /> + <> + <Button onClick={() => setIsOpen(true)}>Open Modal</Button> + <Modal + show={isOpen} + onHide={() => setIsOpen(false)} + title="Example Modal" + primaryButtonName="Submit" + onHandledPrimaryAction={() => { + alert('Submitted!'); + setIsOpen(false); + }} + > + <p>This is the modal content. Click Submit or close the modal.</p> + </Modal> + </> + ); +} +``` + +## Danger Modal + +```tsx live +function DangerModal() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + <Button buttonStyle="danger" onClick={() => setIsOpen(true)}>Delete Item</Button> + <Modal + show={isOpen} + onHide={() => setIsOpen(false)} + title="Confirm Delete" + primaryButtonName="Delete" + primaryButtonStyle="danger" + onHandledPrimaryAction={() => { + alert('Deleted!'); + setIsOpen(false); + }} + > + <p>Are you sure you want to delete this item? This action cannot be undone.</p> + </Modal> + </> + ); +} +``` + +## Confirmation Dialogs + +```tsx live +function ConfirmationDialogs() { + return ( + <div style={{ display: 'flex', gap: 8 }}> + <Button onClick={() => Modal.confirm({ + title: 'Confirm Action', + content: 'Are you sure you want to proceed?', + okText: 'Yes', + })}>Confirm</Button> + <Button onClick={() => Modal.warning({ + title: 'Warning', + content: 'This action may have consequences.', + })}>Warning</Button> + <Button onClick={() => Modal.error({ + title: 'Error', + content: 'Something went wrong.', + })}>Error</Button> + </div> ); } ``` @@ -117,14 +184,14 @@ function Demo() { | Prop | Type | Default | Description | |------|------|---------|-------------| -| `disablePrimaryButton` | `boolean` | `false` | - | -| `primaryButtonName` | `string` | `"Danger"` | - | -| `primaryButtonStyle` | `string` | `"danger"` | The style of the primary action button. | -| `show` | `boolean` | `true` | - | -| `title` | `string` | `"I'm a modal!"` | - | -| `resizable` | `boolean` | `false` | - | -| `draggable` | `boolean` | `false` | - | -| `width` | `number` | `500` | Width of the modal in pixels or as a string. | +| `disablePrimaryButton` | `boolean` | `false` | Whether the primary button is disabled. | +| `primaryButtonName` | `string` | `"Submit"` | Text for the primary action button. | +| `primaryButtonStyle` | `string` | `"primary"` | The style of the primary action button. | +| `show` | `boolean` | `false` | Whether the modal is visible. Use the "Try It" example below for a working demo. | +| `title` | `string` | `"I'm a modal!"` | Title displayed in the modal header. | +| `resizable` | `boolean` | `false` | Whether the modal can be resized by dragging corners. | +| `draggable` | `boolean` | `false` | Whether the modal can be dragged by its header. | +| `width` | `number` | `500` | Width of the modal in pixels. | ## Import diff --git a/docs/developer_portal/components/ui/modaltrigger.mdx b/docs/developer_portal/components/ui/modaltrigger.mdx index 350732bd059..1c3eddd84fc 100644 --- a/docs/developer_portal/components/ui/modaltrigger.mdx +++ b/docs/developer_portal/components/ui/modaltrigger.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # ModalTrigger -The ModalTrigger component from Superset's UI library. +A component that renders a trigger element which opens a modal when clicked. Useful for actions that need confirmation or additional input. ## Live Example @@ -34,66 +34,70 @@ The ModalTrigger component from Superset's UI library. component="ModalTrigger" props={{ isButton: true, - modalTitle: "I am a modal title", - modalBody: "I am a modal body", - modalFooter: "I am a modal footer", - tooltip: "I am a tooltip", + modalTitle: "Modal Title", + modalBody: "This is the modal body content.", + tooltip: "Click to open modal", width: "600px", maxWidth: "1000px", responsive: true, draggable: false, - resizable: false + resizable: false, + triggerNode: "Click to Open Modal" }} controls={[ { name: "isButton", label: "Is Button", - type: "boolean" + type: "boolean", + description: "Whether to wrap the trigger in a button element." }, { name: "modalTitle", label: "Modal Title", - type: "text" + type: "text", + description: "Title displayed in the modal header." }, { name: "modalBody", label: "Modal Body", - type: "text" - }, - { - name: "modalFooter", - label: "Modal Footer", - type: "text" + type: "text", + description: "Content displayed in the modal body." }, { name: "tooltip", label: "Tooltip", - type: "text" + type: "text", + description: "Tooltip text shown on hover over the trigger." }, { name: "width", label: "Width", - type: "text" + type: "text", + description: "Width of the modal (e.g., \"600px\", \"80%\")." }, { name: "maxWidth", label: "Max Width", - type: "text" + type: "text", + description: "Maximum width of the modal." }, { name: "responsive", label: "Responsive", - type: "boolean" + type: "boolean", + description: "Whether the modal should be responsive." }, { name: "draggable", label: "Draggable", - type: "boolean" + type: "boolean", + description: "Whether the modal can be dragged by its header." }, { name: "resizable", label: "Resizable", - type: "boolean" + type: "boolean", + description: "Whether the modal can be resized by dragging corners." } ]} /> @@ -107,32 +111,72 @@ function Demo() { return ( <ModalTrigger isButton - modalTitle="I am a modal title" - modalBody="I am a modal body" - modalFooter="I am a modal footer" - tooltip="I am a tooltip" - width="600px" - maxWidth="1000px" + triggerNode={<span>Click to Open</span>} + modalTitle="Example Modal" + modalBody={<p>This is the modal content. You can put any React elements here.</p>} + width="500px" responsive /> ); } ``` +## With Custom Trigger + +```tsx live +function CustomTrigger() { + return ( + <ModalTrigger + triggerNode={ + <Button buttonStyle="primary"> + <Icons.PlusOutlined /> Add New Item + </Button> + } + modalTitle="Add New Item" + modalBody={ + <div> + <p>Fill out the form to add a new item.</p> + <Input placeholder="Item name" /> + </div> + } + width="400px" + /> + ); +} +``` + +## Draggable & Resizable + +```tsx live +function DraggableModal() { + return ( + <ModalTrigger + isButton + triggerNode={<span>Open Draggable Modal</span>} + modalTitle="Draggable & Resizable" + modalBody={<p>Try dragging the header or resizing from the corners!</p>} + draggable + resizable + width="500px" + /> + ); +} +``` + ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| -| `isButton` | `boolean` | `true` | - | -| `modalTitle` | `string` | `"I am a modal title"` | - | -| `modalBody` | `string` | `"I am a modal body"` | - | -| `modalFooter` | `string` | `"I am a modal footer"` | - | -| `tooltip` | `string` | `"I am a tooltip"` | - | -| `width` | `string` | `"600px"` | - | -| `maxWidth` | `string` | `"1000px"` | - | -| `responsive` | `boolean` | `true` | - | -| `draggable` | `boolean` | `false` | - | -| `resizable` | `boolean` | `false` | - | +| `isButton` | `boolean` | `true` | Whether to wrap the trigger in a button element. | +| `modalTitle` | `string` | `"Modal Title"` | Title displayed in the modal header. | +| `modalBody` | `string` | `"This is the modal body content."` | Content displayed in the modal body. | +| `tooltip` | `string` | `"Click to open modal"` | Tooltip text shown on hover over the trigger. | +| `width` | `string` | `"600px"` | Width of the modal (e.g., "600px", "80%"). | +| `maxWidth` | `string` | `"1000px"` | Maximum width of the modal. | +| `responsive` | `boolean` | `true` | Whether the modal should be responsive. | +| `draggable` | `boolean` | `false` | Whether the modal can be dragged by its header. | +| `resizable` | `boolean` | `false` | Whether the modal can be resized by dragging corners. | +| `triggerNode` | `string` | `"Click to Open Modal"` | The clickable element that opens the modal when clicked. | ## Import diff --git a/docs/developer_portal/components/ui/popover.mdx b/docs/developer_portal/components/ui/popover.mdx index eb4eb8ac803..554b469af16 100644 --- a/docs/developer_portal/components/ui/popover.mdx +++ b/docs/developer_portal/components/ui/popover.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Popover -The Popover component from Superset's UI library. +A floating card that appears when hovering or clicking a trigger element. Supports configurable placement, trigger behavior, and custom content. ## Live Example @@ -42,18 +42,20 @@ The Popover component from Superset's UI library. { name: "content", label: "Content", - type: "text" + type: "text", + description: "Content displayed inside the popover body." }, { name: "title", label: "Title", - type: "text" + type: "text", + description: "Title displayed in the popover header." }, { name: "arrow", label: "Arrow", type: "boolean", - description: "Change arrow's visible state" + description: "Whether to show the popover's arrow pointing to the trigger." }, { name: "color", @@ -64,14 +66,36 @@ The Popover component from Superset's UI library. { name: "placement", label: "Placement", - type: "select" + type: "select", + options: [ + "topLeft", + "top", + "topRight", + "leftTop", + "left", + "leftBottom", + "rightTop", + "right", + "rightBottom", + "bottomLeft", + "bottom", + "bottomRight" + ], + description: "Position of the popover relative to the trigger element." }, { name: "trigger", label: "Trigger", - type: "select" + type: "select", + options: [ + "hover", + "click", + "focus" + ], + description: "Event that triggers the popover to appear." } ]} + sampleChildren={[{"component":"Button","props":{"children":"Hover me"}}]} /> ## Try It @@ -85,8 +109,69 @@ function Demo() { content="Popover sample content" title="Popover title" arrow - color="#fff" - /> + > + <Button>Hover me</Button> + </Popover> + ); +} +``` + +## Click Trigger + +```tsx live +function ClickPopover() { + return ( + <Popover + content="This popover appears on click." + title="Click Popover" + trigger="click" + > + <Button>Click me</Button> + </Popover> + ); +} +``` + +## Placements + +```tsx live +function PlacementsDemo() { + return ( + <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', justifyContent: 'center', padding: '60px 0' }}> + {['top', 'right', 'bottom', 'left'].map(placement => ( + <Popover + key={placement} + content={\`This popover is placed on the \${placement}\`} + title={placement} + placement={placement} + > + <Button>{placement}</Button> + </Popover> + ))} + </div> + ); +} +``` + +## Rich Content + +```tsx live +function RichPopover() { + return ( + <Popover + title="Dashboard Info" + content={ + <div> + <p><strong>Created by:</strong> Admin</p> + <p><strong>Last modified:</strong> Jan 2025</p> + <p><strong>Charts:</strong> 12</p> + </div> + } + > + <Button buttonStyle="primary"> + <Icons.InfoCircleOutlined /> View Details + </Button> + </Popover> ); } ``` @@ -95,9 +180,9 @@ function Demo() { | Prop | Type | Default | Description | |------|------|---------|-------------| -| `content` | `string` | `"Popover sample content"` | - | -| `title` | `string` | `"Popover title"` | - | -| `arrow` | `boolean` | `true` | Change arrow's visible state | +| `content` | `string` | `"Popover sample content"` | Content displayed inside the popover body. | +| `title` | `string` | `"Popover title"` | Title displayed in the popover header. | +| `arrow` | `boolean` | `true` | Whether to show the popover's arrow pointing to the trigger. | | `color` | `string` | `"#fff"` | The background color of the popover. | ## Import diff --git a/docs/developer_portal/components/ui/progressbar.mdx b/docs/developer_portal/components/ui/progressbar.mdx index f7561da5365..b73ae47d14b 100644 --- a/docs/developer_portal/components/ui/progressbar.mdx +++ b/docs/developer_portal/components/ui/progressbar.mdx @@ -33,9 +33,20 @@ Progress bar component for displaying completion status. Supports line, circle, <StoryWithControls component="ProgressBar" props={{ - status: "normal" + percent: 75, + status: "normal", + type: "line", + striped: false, + showInfo: true, + strokeLinecap: "round" }} controls={[ + { + name: "percent", + label: "Percent", + type: "number", + description: "Completion percentage (0-100)." + }, { name: "status", label: "Status", @@ -47,6 +58,52 @@ Progress bar component for displaying completion status. Supports line, circle, "active" ], description: "Current status of the progress bar." + }, + { + name: "type", + label: "Type", + type: "select", + options: [ + "line", + "circle", + "dashboard" + ], + description: "Display type: line, circle, or dashboard gauge." + }, + { + name: "striped", + label: "Striped", + type: "boolean", + description: "Whether to show striped animation on the bar." + }, + { + name: "showInfo", + label: "Show Info", + type: "boolean", + description: "Whether to show the percentage text." + }, + { + name: "strokeLinecap", + label: "Stroke Linecap", + type: "select", + options: [ + "round", + "butt", + "square" + ], + description: "Shape of the progress bar endpoints." + }, + { + name: "strokeColor", + label: "Stroke Color", + type: "color", + description: "Color of the progress bar fill." + }, + { + name: "trailColor", + label: "Trail Color", + type: "color", + description: "Color of the unfilled portion." } ]} /> @@ -59,7 +116,10 @@ Edit the code below to experiment with the component: function Demo() { return ( <ProgressBar + percent={75} status="normal" + type="line" + showInfo /> ); } @@ -106,11 +166,31 @@ function StatusDemo() { } ``` +## Custom Colors + +```tsx live +function CustomColors() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> + <ProgressBar percent={50} strokeColor="#1890ff" /> + <ProgressBar percent={70} strokeColor="#52c41a" /> + <ProgressBar percent={30} strokeColor="#faad14" trailColor="#f0f0f0" /> + <ProgressBar percent={90} strokeColor="#ff4d4f" /> + </div> + ); +} +``` + ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| +| `percent` | `number` | `75` | Completion percentage (0-100). | | `status` | `string` | `"normal"` | Current status of the progress bar. | +| `type` | `string` | `"line"` | Display type: line, circle, or dashboard gauge. | +| `striped` | `boolean` | `false` | Whether to show striped animation on the bar. | +| `showInfo` | `boolean` | `true` | Whether to show the percentage text. | +| `strokeLinecap` | `string` | `"round"` | Shape of the progress bar endpoints. | ## Import diff --git a/docs/developer_portal/components/ui/select.mdx b/docs/developer_portal/components/ui/select.mdx index 24929ecddc4..f84be6b30b0 100644 --- a/docs/developer_portal/components/ui/select.mdx +++ b/docs/developer_portal/components/ui/select.mdx @@ -26,227 +26,134 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Select -The Select component from Superset's UI library. +A versatile select component supporting single and multi-select modes, search filtering, option creation, and both synchronous and asynchronous data sources. ## Live Example <StoryWithControls component="Select" props={{ - autoFocus: true, + mode: "single", + placeholder: "Select ...", + showSearch: true, allowNewOptions: false, allowClear: false, - autoClearSearchValue: false, allowSelectAll: true, disabled: false, - header: "none", invertSelection: false, - labelInValue: true, - maxTagCount: 4, - mode: "single", oneLine: false, - placeholder: "Select ...", - showSearch: true + maxTagCount: 4, + options: [ + { + label: "Such an incredibly awesome long long label", + value: "long-label-1" + }, + { + label: "Another incredibly awesome long long label", + value: "long-label-2" + }, + { + label: "Option A", + value: "A" + }, + { + label: "Option B", + value: "B" + }, + { + label: "Option C", + value: "C" + }, + { + label: "Option D", + value: "D" + }, + { + label: "Option E", + value: "E" + }, + { + label: "Option F", + value: "F" + }, + { + label: "Option G", + value: "G" + }, + { + label: "Option H", + value: "H" + }, + { + label: "Option I", + value: "I" + } + ] }} controls={[ { - name: "autoFocus", - label: "Auto Focus", - type: "boolean" + name: "mode", + label: "Mode", + type: "inline-radio", + options: [ + "single", + "multiple" + ], + description: "Whether to allow selection of a single option or multiple." + }, + { + name: "placeholder", + label: "Placeholder", + type: "text", + description: "Placeholder text when no option is selected." + }, + { + name: "showSearch", + label: "Show Search", + type: "boolean", + description: "Whether to show a search input for filtering." }, { name: "allowNewOptions", label: "Allow New Options", type: "boolean", - description: "It enables the user to create new options. Can be used with standard or async select types. Can be used with any mode, single or multiple. False by default." + description: "Whether users can create new options by typing a value not in the list." }, { name: "allowClear", label: "Allow Clear", - type: "boolean" - }, - { - name: "autoClearSearchValue", - label: "Auto Clear Search Value", - type: "boolean" + type: "boolean", + description: "Whether to show a clear button to reset the selection." }, { name: "allowSelectAll", label: "Allow Select All", - type: "boolean" + type: "boolean", + description: "Whether to show a \"Select All\" option in multiple mode." }, { name: "disabled", label: "Disabled", - type: "boolean" - }, - { - name: "header", - label: "Header", - type: "inline-radio", - options: [ - "none", - "text", - "control" - ], - description: "It adds a header on top of the Select. Can be any ReactNode." + type: "boolean", + description: "Whether the select is disabled." }, { name: "invertSelection", label: "Invert Selection", type: "boolean", - description: "It shows a stop-outlined icon at the far right of a selected option instead of the default checkmark. Useful to better indicate to the user that by clicking on a selected option it will be de-selected. False by default." - }, - { - name: "labelInValue", - label: "Label In Value", - type: "boolean" - }, - { - name: "maxTagCount", - label: "Max Tag Count", - type: "number", - description: "Sets maxTagCount attribute. The overflow tag is displayed in place of the remaining items. Requires '\"mode=multiple\"'." - }, - { - name: "mode", - label: "Mode", - type: "inline-radio", - options: [ - "single", - "multiple" - ], - description: "It defines whether the Select should allow for the selection of multiple options or single. Single by default." + description: "Shows a stop icon instead of a checkmark on selected options, indicating deselection on click." }, { name: "oneLine", label: "One Line", type: "boolean", - description: "Sets maxTagCount to 1. The overflow tag is always displayed in the same line, line wrapping is disabled. When the dropdown is open, sets maxTagCount to 0, displays only the overflow tag. Requires '\"mode=multiple\"'." - }, - { - name: "placeholder", - label: "Placeholder", - type: "text" - }, - { - name: "showSearch", - label: "Show Search", - type: "boolean" - }, - { - name: "options", - label: "Options", - type: "select", - options: [ - "Such an incredibly awesome long long label", - "Such an incredibly awesome long long label", - "Secret custom prop", - "Another incredibly awesome long long label", - "Another incredibly awesome long long label", - "red", - "JSX Label", - "A", - "A", - "B", - "B", - "C", - "C", - "D", - "D", - "E", - "E", - "F", - "F", - "G", - "G", - "H", - "H", - "I", - "I" - ], - description: "It defines the options of the Select. The options can be static, an array of options. The options can also be async, a promise that returns an array of options." - }, - { - name: "control", - label: "Control", - type: "select", - options: [ - "none", - "text", - "control" - ] - }, - { - name: "optionsCount", - label: "Options Count", - type: "number" + description: "Forces tags onto one line with overflow count. Requires multiple mode." }, { - name: "pageSize", - label: "Page Size", - type: "select", - options: [ - "Such an incredibly awesome long long label", - "Such an incredibly awesome long long label", - "Secret custom prop", - "Another incredibly awesome long long label", - "Another incredibly awesome long long label", - "red", - "JSX Label", - "A", - "A", - "B", - "B", - "C", - "C", - "D", - "D", - "E", - "E", - "F", - "F", - "G", - "G", - "H", - "H", - "I", - "I" - ], - description: "It defines how many results should be included in the query response. Works in async mode only (See the options property)." - }, - { - name: "fetchOnlyOnSearch", - label: "Fetch Only On Search", - type: "select", - options: [ - "Such an incredibly awesome long long label", - "Such an incredibly awesome long long label", - "Secret custom prop", - "Another incredibly awesome long long label", - "Another incredibly awesome long long label", - "red", - "JSX Label", - "A", - "A", - "B", - "B", - "C", - "C", - "D", - "D", - "E", - "E", - "F", - "F", - "G", - "G", - "H", - "H", - "I", - "I" - ], - description: "It fires a request against the server only after searching. Works in async mode only (See the options property). Undefined by default." + name: "maxTagCount", + label: "Max Tag Count", + type: "number", + description: "Maximum number of tags to display in multiple mode before showing an overflow count." } ]} /> @@ -258,16 +165,115 @@ Edit the code below to experiment with the component: ```tsx live function Demo() { return ( - <Select - autoFocus - allowSelectAll - header="none" - labelInValue - maxTagCount={4} - mode="single" - placeholder="Select ..." - showSearch - /> + <div style={{ width: 300 }}> + <Select + ariaLabel="demo-select" + options={[ + { label: 'Dashboards', value: 'dashboards' }, + { label: 'Charts', value: 'charts' }, + { label: 'Datasets', value: 'datasets' }, + { label: 'SQL Lab', value: 'sqllab' }, + { label: 'Settings', value: 'settings' }, + ]} + placeholder="Select ..." + showSearch + /> + </div> + ); +} +``` + +## Multi Select + +```tsx live +function MultiSelectDemo() { + return ( + <div style={{ width: 400 }}> + <Select + ariaLabel="multi-select" + mode="multiple" + options={[ + { label: 'Dashboards', value: 'dashboards' }, + { label: 'Charts', value: 'charts' }, + { label: 'Datasets', value: 'datasets' }, + { label: 'SQL Lab', value: 'sqllab' }, + { label: 'Settings', value: 'settings' }, + ]} + placeholder="Select items..." + allowSelectAll + maxTagCount={3} + /> + </div> + ); +} +``` + +## Allow New Options + +```tsx live +function AllowNewDemo() { + return ( + <div style={{ width: 300 }}> + <Select + ariaLabel="allow-new-select" + mode="multiple" + options={[ + { label: 'Red', value: 'red' }, + { label: 'Green', value: 'green' }, + { label: 'Blue', value: 'blue' }, + ]} + placeholder="Type to add tags..." + allowNewOptions + showSearch + /> + </div> + ); +} +``` + +## Inverted Selection + +```tsx live +function InvertedDemo() { + return ( + <div style={{ width: 400 }}> + <Select + ariaLabel="inverted-select" + mode="multiple" + options={[ + { label: 'Admin', value: 'admin' }, + { label: 'Editor', value: 'editor' }, + { label: 'Viewer', value: 'viewer' }, + { label: 'Public', value: 'public' }, + ]} + placeholder="Exclude roles..." + invertSelection + /> + </div> + ); +} +``` + +## One Line Mode + +```tsx live +function OneLineDemo() { + return ( + <div style={{ width: 300 }}> + <Select + ariaLabel="oneline-select" + mode="multiple" + options={[ + { label: 'Dashboard 1', value: 'd1' }, + { label: 'Dashboard 2', value: 'd2' }, + { label: 'Dashboard 3', value: 'd3' }, + { label: 'Dashboard 4', value: 'd4' }, + { label: 'Dashboard 5', value: 'd5' }, + ]} + placeholder="Select dashboards..." + oneLine + /> + </div> ); } ``` @@ -276,20 +282,17 @@ function Demo() { | Prop | Type | Default | Description | |------|------|---------|-------------| -| `autoFocus` | `boolean` | `true` | - | -| `allowNewOptions` | `boolean` | `false` | It enables the user to create new options. Can be used with standard or async select types. Can be used with any mode, single or multiple. False by default. | -| `allowClear` | `boolean` | `false` | - | -| `autoClearSearchValue` | `boolean` | `false` | - | -| `allowSelectAll` | `boolean` | `true` | - | -| `disabled` | `boolean` | `false` | - | -| `header` | `string` | `"none"` | It adds a header on top of the Select. Can be any ReactNode. | -| `invertSelection` | `boolean` | `false` | It shows a stop-outlined icon at the far right of a selected option instead of the default checkmark. Useful to better indicate to the user that by clicking on a selected option it will be de-selected. False by default. | -| `labelInValue` | `boolean` | `true` | - | -| `maxTagCount` | `number` | `4` | Sets maxTagCount attribute. The overflow tag is displayed in place of the remaining items. Requires '"mode=multiple"'. | -| `mode` | `string` | `"single"` | It defines whether the Select should allow for the selection of multiple options or single. Single by default. | -| `oneLine` | `boolean` | `false` | Sets maxTagCount to 1. The overflow tag is always displayed in the same line, line wrapping is disabled. When the dropdown is open, sets maxTagCount to 0, displays only the overflow tag. Requires '"mode=multiple"'. | -| `placeholder` | `string` | `"Select ..."` | - | -| `showSearch` | `boolean` | `true` | - | +| `mode` | `string` | `"single"` | Whether to allow selection of a single option or multiple. | +| `placeholder` | `string` | `"Select ..."` | Placeholder text when no option is selected. | +| `showSearch` | `boolean` | `true` | Whether to show a search input for filtering. | +| `allowNewOptions` | `boolean` | `false` | Whether users can create new options by typing a value not in the list. | +| `allowClear` | `boolean` | `false` | Whether to show a clear button to reset the selection. | +| `allowSelectAll` | `boolean` | `true` | Whether to show a "Select All" option in multiple mode. | +| `disabled` | `boolean` | `false` | Whether the select is disabled. | +| `invertSelection` | `boolean` | `false` | Shows a stop icon instead of a checkmark on selected options, indicating deselection on click. | +| `oneLine` | `boolean` | `false` | Forces tags onto one line with overflow count. Requires multiple mode. | +| `maxTagCount` | `number` | `4` | Maximum number of tags to display in multiple mode before showing an overflow count. | +| `options` | `any` | `[{"label":"Such an incredibly awesome long long label","value":"long-label-1"},{"label":"Another incredibly awesome long long label","value":"long-label-2"},{"label":"Option A","value":"A"},{"label":"Option B","value":"B"},{"label":"Option C","value":"C"},{"label":"Option D","value":"D"},{"label":"Option E","value":"E"},{"label":"Option F","value":"F"},{"label":"Option G","value":"G"},{"label":"Option H","value":"H"},{"label":"Option I","value":"I"}]` | - | ## Import diff --git a/docs/developer_portal/components/ui/slider.mdx b/docs/developer_portal/components/ui/slider.mdx index e32ba506eab..e92e20badd5 100644 --- a/docs/developer_portal/components/ui/slider.mdx +++ b/docs/developer_portal/components/ui/slider.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Slider -The Slider component from Superset's UI library. +A slider input for selecting a value or range from a continuous or stepped interval. Supports single value, range, vertical orientation, marks, and tooltip display. ## Live Example @@ -40,67 +40,76 @@ The Slider component from Superset's UI library. disabled: false, reverse: false, vertical: false, - autoFocus: false, keyboard: true, dots: false, - included: true, - tooltipPosition: "bottom" + included: true }} controls={[ { name: "min", label: "Min", - type: "number" + type: "number", + description: "Minimum value of the slider." }, { name: "max", label: "Max", - type: "number" + type: "number", + description: "Maximum value of the slider." }, { name: "defaultValue", label: "Default Value", - type: "number" + type: "number", + description: "Initial value of the slider." }, { name: "step", label: "Step", - type: "number" + type: "number", + description: "Step increment between values. Use null for marks-only mode." }, { name: "disabled", label: "Disabled", - type: "boolean" + type: "boolean", + description: "Whether the slider is disabled." }, { name: "reverse", label: "Reverse", - type: "boolean" + type: "boolean", + description: "Whether to reverse the slider direction." }, { name: "vertical", label: "Vertical", - type: "boolean" - }, - { - name: "autoFocus", - label: "Auto Focus", - type: "boolean" + type: "boolean", + description: "Whether to display the slider vertically." }, { name: "keyboard", label: "Keyboard", - type: "boolean" + type: "boolean", + description: "Whether keyboard arrow keys can control the slider." }, { name: "dots", label: "Dots", - type: "boolean" + type: "boolean", + description: "Whether to show dots at each step mark." }, { name: "included", label: "Included", - type: "boolean" + type: "boolean", + description: "Whether to highlight the filled portion of the track." + }, + { + name: "tooltipOpen", + label: "Tooltip Open", + type: "boolean", + description: "Whether the value tooltip is always visible." }, { name: "tooltipPosition", @@ -119,12 +128,8 @@ The Slider component from Superset's UI library. "leftBottom", "rightTop", "rightBottom" - ] - }, - { - name: "tooltipOpen", - label: "Tooltip Open", - type: "boolean" + ], + description: "Position of the value tooltip relative to the handle." } ]} /> @@ -136,15 +141,85 @@ Edit the code below to experiment with the component: ```tsx live function Demo() { return ( - <Slider - min={0} - max={100} - defaultValue={70} - step={1} - keyboard - included - tooltipPosition="bottom" - /> + <div style={{ width: 400, padding: '20px 0' }}> + <Slider + min={0} + max={100} + defaultValue={70} + step={1} + /> + </div> + ); +} +``` + +## Range Slider + +```tsx live +function RangeSliderDemo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <h4>Basic Range</h4> + <Slider range defaultValue={[20, 70]} min={0} max={100} /> + <br /> + <h4>Draggable Track</h4> + <Slider range={{ draggableTrack: true }} defaultValue={[30, 60]} min={0} max={100} /> + </div> + ); +} +``` + +## With Marks + +```tsx live +function MarksDemo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <Slider + min={0} + max={100} + defaultValue={37} + marks={{ + 0: '0°C', + 25: '25°C', + 50: '50°C', + 75: '75°C', + 100: '100°C', + }} + /> + </div> + ); +} +``` + +## Stepped and Dots + +```tsx live +function SteppedDemo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <h4>Step = 10 with Dots</h4> + <Slider min={0} max={100} defaultValue={30} step={10} dots /> + <br /> + <h4>Step = 25</h4> + <Slider min={0} max={100} defaultValue={50} step={25} dots + marks={{ 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }} /> + </div> + ); +} +``` + +## Vertical Slider + +```tsx live +function VerticalDemo() { + return ( + <div style={{ height: 300, display: 'flex', gap: 40, padding: '0 40px' }}> + <Slider vertical defaultValue={30} /> + <Slider vertical range defaultValue={[20, 60]} /> + <Slider vertical defaultValue={50} dots step={10} + marks={{ 0: '0', 50: '50', 100: '100' }} /> + </div> ); } ``` @@ -153,18 +228,16 @@ function Demo() { | Prop | Type | Default | Description | |------|------|---------|-------------| -| `min` | `number` | `0` | - | -| `max` | `number` | `100` | - | -| `defaultValue` | `number` | `70` | - | -| `step` | `number` | `1` | - | -| `disabled` | `boolean` | `false` | - | -| `reverse` | `boolean` | `false` | - | -| `vertical` | `boolean` | `false` | - | -| `autoFocus` | `boolean` | `false` | - | -| `keyboard` | `boolean` | `true` | - | -| `dots` | `boolean` | `false` | - | -| `included` | `boolean` | `true` | - | -| `tooltipPosition` | `string` | `"bottom"` | - | +| `min` | `number` | `0` | Minimum value of the slider. | +| `max` | `number` | `100` | Maximum value of the slider. | +| `defaultValue` | `number` | `70` | Initial value of the slider. | +| `step` | `number` | `1` | Step increment between values. Use null for marks-only mode. | +| `disabled` | `boolean` | `false` | Whether the slider is disabled. | +| `reverse` | `boolean` | `false` | Whether to reverse the slider direction. | +| `vertical` | `boolean` | `false` | Whether to display the slider vertically. | +| `keyboard` | `boolean` | `true` | Whether keyboard arrow keys can control the slider. | +| `dots` | `boolean` | `false` | Whether to show dots at each step mark. | +| `included` | `boolean` | `true` | Whether to highlight the filled portion of the track. | ## Import diff --git a/docs/developer_portal/components/ui/steps.mdx b/docs/developer_portal/components/ui/steps.mdx index b9354e71671..bf76e0681c9 100644 --- a/docs/developer_portal/components/ui/steps.mdx +++ b/docs/developer_portal/components/ui/steps.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Steps -The Steps component from Superset's UI library. +A navigation component for guiding users through multi-step workflows. Supports horizontal, vertical, and inline layouts with progress tracking. ## Live Example @@ -34,14 +34,28 @@ The Steps component from Superset's UI library. component="Steps" props={{ direction: "horizontal", - initial: 0, + current: 1, labelPlacement: "horizontal", progressDot: false, size: "default", status: "process", type: "default", title: "Step 3", - description: "Description 3" + description: "Description 3", + items: [ + { + title: "Connect Database", + description: "Configure the connection" + }, + { + title: "Create Dataset", + description: "Select tables and columns" + }, + { + title: "Build Chart", + description: "Choose visualization type" + } + ] }} controls={[ { @@ -51,12 +65,14 @@ The Steps component from Superset's UI library. options: [ "horizontal", "vertical" - ] + ], + description: "Layout direction of the steps." }, { - name: "initial", - label: "Initial", - type: "number" + name: "current", + label: "Current", + type: "number", + description: "Index of the current step (zero-based)." }, { name: "labelPlacement", @@ -65,12 +81,14 @@ The Steps component from Superset's UI library. options: [ "horizontal", "vertical" - ] + ], + description: "Position of step labels relative to the step icon." }, { name: "progressDot", label: "Progress Dot", - type: "boolean" + type: "boolean", + description: "Whether to use a dot style instead of numbered icons." }, { name: "size", @@ -79,7 +97,8 @@ The Steps component from Superset's UI library. options: [ "default", "small" - ] + ], + description: "Size of the step icons and text." }, { name: "status", @@ -90,7 +109,8 @@ The Steps component from Superset's UI library. "process", "finish", "error" - ] + ], + description: "Status of the current step." }, { name: "type", @@ -100,7 +120,8 @@ The Steps component from Superset's UI library. "default", "navigation", "inline" - ] + ], + description: "Visual style: default numbered, navigation breadcrumb, or inline compact." }, { name: "title", @@ -123,32 +144,119 @@ Edit the code below to experiment with the component: function Demo() { return ( <Steps - direction="horizontal" - initial={0} - labelPlacement="horizontal" - size="default" - status="process" - type="default" - title="Step 3" - description="Description 3" + current={1} + items={[ + { title: 'Connect Database', description: 'Configure the connection' }, + { title: 'Create Dataset', description: 'Select tables and columns' }, + { title: 'Build Chart', description: 'Choose visualization type' }, + ]} /> ); } ``` +## Vertical Steps + +```tsx live +function VerticalSteps() { + return ( + <Steps + direction="vertical" + current={1} + items={[ + { title: 'Upload CSV', description: 'Select a file from your computer' }, + { title: 'Configure Columns', description: 'Set data types and names' }, + { title: 'Review', description: 'Verify the data looks correct' }, + { title: 'Import', description: 'Save the dataset' }, + ]} + /> + ); +} +``` + +## Status Indicators + +```tsx live +function StatusSteps() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> + <div> + <h4>Error on Step 2</h4> + <Steps + current={1} + status="error" + items={[ + { title: 'Connection', description: 'Configured' }, + { title: 'Validation', description: 'Failed to validate' }, + { title: 'Complete' }, + ]} + /> + </div> + <div> + <h4>All Complete</h4> + <Steps + current={3} + items={[ + { title: 'Step 1' }, + { title: 'Step 2' }, + { title: 'Step 3' }, + ]} + /> + </div> + </div> + ); +} +``` + +## Dot Style and Small Size + +```tsx live +function DotAndSmall() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> + <div> + <h4>Progress Dots</h4> + <Steps + progressDot + current={1} + items={[ + { title: 'Create', description: 'Define the resource' }, + { title: 'Configure', description: 'Set parameters' }, + { title: 'Deploy', description: 'Go live' }, + ]} + /> + </div> + <div> + <h4>Small Size</h4> + <Steps + size="small" + current={2} + items={[ + { title: 'Login' }, + { title: 'Verify' }, + { title: 'Done' }, + ]} + /> + </div> + </div> + ); +} +``` + ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| -| `direction` | `string` | `"horizontal"` | - | -| `initial` | `number` | `0` | - | -| `labelPlacement` | `string` | `"horizontal"` | - | -| `progressDot` | `boolean` | `false` | - | -| `size` | `string` | `"default"` | - | -| `status` | `string` | `"process"` | - | -| `type` | `string` | `"default"` | - | +| `direction` | `string` | `"horizontal"` | Layout direction of the steps. | +| `current` | `number` | `1` | Index of the current step (zero-based). | +| `labelPlacement` | `string` | `"horizontal"` | Position of step labels relative to the step icon. | +| `progressDot` | `boolean` | `false` | Whether to use a dot style instead of numbered icons. | +| `size` | `string` | `"default"` | Size of the step icons and text. | +| `status` | `string` | `"process"` | Status of the current step. | +| `type` | `string` | `"default"` | Visual style: default numbered, navigation breadcrumb, or inline compact. | | `title` | `string` | `"Step 3"` | - | | `description` | `string` | `"Description 3"` | - | +| `items` | `any` | `[{"title":"Connect Database","description":"Configure the connection"},{"title":"Create Dataset","description":"Select tables and columns"},{"title":"Build Chart","description":"Choose visualization type"}]` | - | ## Import diff --git a/docs/developer_portal/components/ui/switch.mdx b/docs/developer_portal/components/ui/switch.mdx index 44db07d022e..dc87c617f46 100644 --- a/docs/developer_portal/components/ui/switch.mdx +++ b/docs/developer_portal/components/ui/switch.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # Switch -The Switch component from Superset's UI library. +A toggle switch for boolean on/off states. Supports loading indicators, sizing, and an HTML title attribute for accessibility tooltips. ## Live Example @@ -35,29 +35,32 @@ The Switch component from Superset's UI library. props={{ disabled: false, loading: false, - title: "Switch", - autoFocus: true + title: "Toggle feature" }} controls={[ { name: "disabled", label: "Disabled", - type: "boolean" + type: "boolean", + description: "Whether the switch is disabled." }, { name: "loading", label: "Loading", - type: "boolean" + type: "boolean", + description: "Whether to show a loading spinner inside the switch." }, { name: "title", label: "Title", - type: "text" + type: "text", + description: "HTML title attribute shown as a browser tooltip on hover. Useful for accessibility." }, { - name: "autoFocus", - label: "Auto Focus", - type: "boolean" + name: "checked", + label: "Checked", + type: "boolean", + description: "Whether the switch is on." }, { name: "size", @@ -66,7 +69,8 @@ The Switch component from Superset's UI library. options: [ "small", "default" - ] + ], + description: "Size of the switch." } ]} /> @@ -77,11 +81,92 @@ Edit the code below to experiment with the component: ```tsx live function Demo() { + const [checked, setChecked] = React.useState(true); return ( - <Switch - title="Switch" - autoFocus - /> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={checked} + onChange={setChecked} + title="Toggle feature" + /> + <span>{checked ? 'On' : 'Off'}</span> + <span style={{ color: '#999', fontSize: 12 }}>(hover the switch to see the title tooltip)</span> + </div> + ); +} +``` + +## Switch States + +```tsx live +function SwitchStates() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch defaultChecked title="Enabled switch" /> + <span>Checked</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch title="Unchecked switch" /> + <span>Unchecked</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch disabled defaultChecked title="Disabled on" /> + <span>Disabled (on)</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch disabled title="Disabled off" /> + <span>Disabled (off)</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch loading defaultChecked title="Loading switch" /> + <span>Loading</span> + </div> + </div> + ); +} +``` + +## Sizes + +```tsx live +function SizesDemo() { + return ( + <div style={{ display: 'flex', alignItems: 'center', gap: 24 }}> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch size="small" defaultChecked title="Small switch" /> + <span>Small</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch size="default" defaultChecked title="Default switch" /> + <span>Default</span> + </div> + </div> + ); +} +``` + +## Settings Panel + +```tsx live +function SettingsPanel() { + const [notifications, setNotifications] = React.useState(true); + const [darkMode, setDarkMode] = React.useState(false); + const [autoRefresh, setAutoRefresh] = React.useState(true); + return ( + <div style={{ maxWidth: 320, border: '1px solid #e8e8e8', borderRadius: 8, padding: 16 }}> + <h4 style={{ marginTop: 0 }}>Dashboard Settings</h4> + {[ + { label: 'Email notifications', checked: notifications, onChange: setNotifications, title: 'Toggle email notifications' }, + { label: 'Dark mode', checked: darkMode, onChange: setDarkMode, title: 'Toggle dark mode' }, + { label: 'Auto-refresh data', checked: autoRefresh, onChange: setAutoRefresh, title: 'Toggle auto-refresh' }, + ].map(({ label, checked, onChange, title }) => ( + <div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid #f0f0f0' }}> + <span>{label}</span> + <Switch checked={checked} onChange={onChange} title={title} /> + </div> + ))} + </div> ); } ``` @@ -90,10 +175,9 @@ function Demo() { | Prop | Type | Default | Description | |------|------|---------|-------------| -| `disabled` | `boolean` | `false` | - | -| `loading` | `boolean` | `false` | - | -| `title` | `string` | `"Switch"` | - | -| `autoFocus` | `boolean` | `true` | - | +| `disabled` | `boolean` | `false` | Whether the switch is disabled. | +| `loading` | `boolean` | `false` | Whether to show a loading spinner inside the switch. | +| `title` | `string` | `"Toggle feature"` | HTML title attribute shown as a browser tooltip on hover. Useful for accessibility. | ## Import diff --git a/docs/developer_portal/components/ui/tableview.mdx b/docs/developer_portal/components/ui/tableview.mdx index 6ad4c615caa..b1446aac650 100644 --- a/docs/developer_portal/components/ui/tableview.mdx +++ b/docs/developer_portal/components/ui/tableview.mdx @@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper'; # TableView -The TableView component from Superset's UI library. +A data table component with sorting, pagination, text wrapping, and empty state support. Built on react-table. ## Live Example @@ -36,14 +36,57 @@ The TableView component from Superset's UI library. accessor: "summary", Header: "Summary", sortable: true, - id: 321, + id: 456, age: 10, name: "John Smith", noDataText: "No data here", - pageSize: 1, + pageSize: 2, showRowCount: true, withPagination: true, - scrollTopOnPagination: false + scrollTopOnPagination: false, + columns: [ + { + accessor: "id", + Header: "ID", + sortable: true, + id: "id" + }, + { + accessor: "age", + Header: "Age", + id: "age" + }, + { + accessor: "name", + Header: "Name", + id: "name" + }, + { + accessor: "summary", + Header: "Summary", + id: "summary" + } + ], + data: [ + { + id: 123, + age: 27, + name: "Emily", + summary: "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + }, + { + id: 321, + age: 10, + name: "Kate", + summary: "Nam id porta neque, a vehicula orci." + }, + { + id: 456, + age: 10, + name: "John Smith", + summary: "Maecenas rhoncus elit sit amet purus convallis placerat." + } + ] }} controls={[ { @@ -79,37 +122,44 @@ The TableView component from Superset's UI library. { name: "noDataText", label: "No Data Text", - type: "text" + type: "text", + description: "Text displayed when the table has no data." }, { name: "pageSize", label: "Page Size", - type: "number" + type: "number", + description: "Number of rows displayed per page." }, { name: "showRowCount", label: "Show Row Count", - type: "boolean" + type: "boolean", + description: "Whether to display the total row count alongside pagination." }, { name: "withPagination", label: "With Pagination", - type: "boolean" + type: "boolean", + description: "Whether to show pagination controls below the table." }, { name: "scrollTopOnPagination", label: "Scroll Top On Pagination", - type: "boolean" + type: "boolean", + description: "Whether to scroll to the top of the table when changing pages." }, { name: "emptyWrapperType", label: "Empty Wrapper Type", - type: "select" + type: "select", + description: "Style of the empty state wrapper." }, { name: "initialPageIndex", label: "Initial Page Index", - type: "number" + type: "number", + description: "Initial page to display (zero-based)." } ]} /> @@ -122,16 +172,84 @@ Edit the code below to experiment with the component: function Demo() { return ( <TableView - accessor="summary" - Header="Summary" - sortable - id={321} - age={10} - name="John Smith" - noDataText="No data here" - pageSize={1} - showRowCount + columns={[ + { accessor: 'id', Header: 'ID', sortable: true, id: 'id' }, + { accessor: 'age', Header: 'Age', id: 'age' }, + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'summary', Header: 'Summary', id: 'summary' }, + ]} + data={[ + { id: 123, age: 27, name: 'Emily', summary: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' }, + { id: 321, age: 10, name: 'Kate', summary: 'Nam id porta neque, a vehicula orci.' }, + { id: 456, age: 10, name: 'John Smith', summary: 'Maecenas rhoncus elit sit amet purus convallis placerat.' }, + ]} + initialSortBy={[{ id: 'name', desc: true }]} + pageSize={2} withPagination + showRowCount + /> + ); +} +``` + +## Without Pagination + +```tsx live +function NoPaginationDemo() { + return ( + <TableView + columns={[ + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'email', Header: 'Email', id: 'email' }, + { accessor: 'status', Header: 'Status', id: 'status' }, + ]} + data={[ + { name: 'Alice', email: '[email protected]', status: 'Active' }, + { name: 'Bob', email: '[email protected]', status: 'Inactive' }, + { name: 'Charlie', email: '[email protected]', status: 'Active' }, + ]} + withPagination={false} + /> + ); +} +``` + +## Empty State + +```tsx live +function EmptyDemo() { + return ( + <TableView + columns={[ + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'value', Header: 'Value', id: 'value' }, + ]} + data={[]} + noDataText="No results found" + /> + ); +} +``` + +## With Sorting + +```tsx live +function SortingDemo() { + return ( + <TableView + columns={[ + { accessor: 'id', Header: 'ID', id: 'id', sortable: true }, + { accessor: 'name', Header: 'Name', id: 'name', sortable: true }, + { accessor: 'score', Header: 'Score', id: 'score', sortable: true }, + ]} + data={[ + { id: 1, name: 'Dashboard A', score: 95 }, + { id: 2, name: 'Dashboard B', score: 72 }, + { id: 3, name: 'Dashboard C', score: 88 }, + { id: 4, name: 'Dashboard D', score: 64 }, + ]} + initialSortBy={[{ id: 'score', desc: true }]} + withPagination={false} /> ); } @@ -144,14 +262,16 @@ function Demo() { | `accessor` | `string` | `"summary"` | - | | `Header` | `string` | `"Summary"` | - | | `sortable` | `boolean` | `true` | - | -| `id` | `number` | `321` | - | +| `id` | `number` | `456` | - | | `age` | `number` | `10` | - | | `name` | `string` | `"John Smith"` | - | -| `noDataText` | `string` | `"No data here"` | - | -| `pageSize` | `number` | `1` | - | -| `showRowCount` | `boolean` | `true` | - | -| `withPagination` | `boolean` | `true` | - | -| `scrollTopOnPagination` | `boolean` | `false` | - | +| `noDataText` | `string` | `"No data here"` | Text displayed when the table has no data. | +| `pageSize` | `number` | `2` | Number of rows displayed per page. | +| `showRowCount` | `boolean` | `true` | Whether to display the total row count alongside pagination. | +| `withPagination` | `boolean` | `true` | Whether to show pagination controls below the table. | +| `scrollTopOnPagination` | `boolean` | `false` | Whether to scroll to the top of the table when changing pages. | +| `columns` | `any` | `[{"accessor":"id","Header":"ID","sortable":true,"id":"id"},{"accessor":"age","Header":"Age","id":"age"},{"accessor":"name","Header":"Name","id":"name"},{"accessor":"summary","Header":"Summary","id":"summary"}]` | - | +| `data` | `any` | `[{"id":123,"age":27,"name":"Emily","summary":"Lorem ipsum dolor sit amet, consectetur adipiscing elit."},{"id":321,"age":10,"name":"Kate","summary":"Nam id porta neque, a vehicula orci."},{"id":456,"age":10,"name":"John Smith","summary":"Maecenas rhoncus elit sit amet purus convallis placerat."}]` | - | ## Import diff --git a/docs/scripts/generate-superset-components.mjs b/docs/scripts/generate-superset-components.mjs index a7a8b26cd66..c8db5e43d6e 100644 --- a/docs/scripts/generate-superset-components.mjs +++ b/docs/scripts/generate-superset-components.mjs @@ -557,6 +557,13 @@ function parseArgTypes(argTypesContent, argTypes, fullContent) { } else if (controlObjectMatch) { argTypes[propName].type = controlObjectMatch[1]; } + + // Clear options for non-select/radio types (the shorthand "options" detection + // can false-positive when the word "options" appears in description text) + const finalType = argTypes[propName].type; + if (finalType && !['select', 'radio', 'inline-radio'].includes(finalType)) { + delete argTypes[propName].options; + } } } @@ -658,6 +665,8 @@ function extractDocsConfig(content, storyNames) { let liveExample = null; let examples = null; let renderComponent = null; + let triggerProp = null; + let onHideProp = null; for (const storyName of storyNames) { // Look for parameters block @@ -764,6 +773,16 @@ function extractDocsConfig(content, storyNames) { renderComponent = renderComponentMatch[1]; } + // Extract triggerProp/onHideProp - for components like Modal that need a trigger button + const triggerPropMatch = parametersContent.match(/triggerProp:\s*['"]([^'"]+)['"]/); + if (triggerPropMatch) { + triggerProp = triggerPropMatch[1]; + } + const onHidePropMatch = parametersContent.match(/onHideProp:\s*['"]([^'"]+)['"]/); + if (onHidePropMatch) { + onHideProp = onHidePropMatch[1]; + } + // Extract examples array - for multiple code examples // Format: examples: [{ title: 'Title', code: `...` }, ...] const examplesMatch = parametersContent.match(/examples:\s*\[/); @@ -795,10 +814,10 @@ function extractDocsConfig(content, storyNames) { } } - if (sampleChildren || gallery || staticProps || liveExample || examples || renderComponent) break; + if (sampleChildren || gallery || staticProps || liveExample || examples || renderComponent || triggerProp) break; } - return { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent }; + return { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp }; } /** @@ -832,7 +851,7 @@ function extractArgsAndControls(content, componentName) { const storyNames = [`Interactive${componentName}`, `${componentName}Story`, componentName]; // Extract docs config (sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample) from parameters.docs - const { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent } = extractDocsConfig(content, storyNames); + const { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractDocsConfig(content, storyNames); for (const storyName of storyNames) { // Try CSF 3.0 format: export const StoryName: StoryObj = { args: {...}, argTypes: {...} } @@ -941,7 +960,7 @@ function extractArgsAndControls(content, componentName) { }); } - return { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent }; + return { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp }; } /** @@ -950,7 +969,7 @@ function extractArgsAndControls(content, componentName) { function generateMDX(component, storyContent) { const { componentName, description, relativePath, category, title, sourceConfig, resolvedImportPath, isDefaultExport } = component; - const { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent } = extractArgsAndControls(storyContent, componentName); + const { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractArgsAndControls(storyContent, componentName); // Merge staticProps into args for complex values (arrays, objects) that can't be parsed from inline args const mergedArgs = { ...args, ...staticProps }; @@ -1056,7 +1075,9 @@ ${hasGallery ? ` props={${propsJson}} controls={${controlsJson}}${sampleChildrenJson ? ` sampleChildren={${sampleChildrenJson}}` : ''}${sampleChildrenStyleJson ? ` - sampleChildrenStyle={${sampleChildrenStyleJson}}` : ''} + sampleChildrenStyle={${sampleChildrenStyleJson}}` : ''}${triggerProp ? ` + triggerProp="${triggerProp}"` : ''}${onHideProp ? ` + onHideProp="${onHideProp}"` : ''} /> ## Try It diff --git a/docs/src/components/StorybookWrapper.jsx b/docs/src/components/StorybookWrapper.jsx index ecc774fdda6..fca8a2342d7 100644 --- a/docs/src/components/StorybookWrapper.jsx +++ b/docs/src/components/StorybookWrapper.jsx @@ -243,7 +243,8 @@ function generateSampleChildren(sampleChildren, sampleChildrenStyle) { // Inner component for StoryWithControls (browser-only) // renderComponent allows overriding which component to actually render (useful when the named // component is a namespace object like Icons, not a React component) -function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle }) { +// triggerProp: for components like Modal that need a trigger, specify the boolean prop that controls visibility +function StoryWithControlsInner({ component, renderComponent, props, controls, sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) { // Use renderComponent if provided, otherwise use the main component name const componentToRender = renderComponent || component; const Component = resolveComponent(componentToRender); @@ -258,12 +259,28 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s }; // Extract children from props (label, children, text, content) - const { children: propsChildren, restProps } = extractChildren(stateProps); + // When sampleChildren is explicitly provided, skip extraction so all props + // (like 'content') stay as component props rather than becoming children + const { children: propsChildren, restProps } = sampleChildren + ? { children: null, restProps: stateProps } + : extractChildren(stateProps); // Filter out undefined values so they don't override component defaults const filteredProps = Object.fromEntries( Object.entries(restProps).filter(([, v]) => v !== undefined) ); + // Resolve any prop values that are component descriptors + // e.g., { component: 'Button', props: { children: 'Click' } } + Object.keys(filteredProps).forEach(key => { + const value = filteredProps[key]; + if (value && typeof value === 'object' && value.component) { + const PropComponent = resolveComponent(value.component); + if (PropComponent) { + filteredProps[key] = <PropComponent {...value.props} />; + } + } + }); + // For List-like components with dataSource but no renderItem, provide a default if (filteredProps.dataSource && !filteredProps.renderItem) { const ListItem = resolveComponent('List')?.Item; @@ -276,6 +293,15 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s // Use sample children if provided, otherwise use props children const children = generateSampleChildren(sampleChildren, sampleChildrenStyle) || propsChildren; + // For components with a trigger (like Modal with show/onHide), add handlers + const triggerProps = {}; + if (triggerProp && onHideProp) { + triggerProps[onHideProp] = () => updateProp(triggerProp, false); + } + + // Get the Button component for trigger buttons + const ButtonComponent = resolveComponent('Button'); + return ( <Providers> <div className="storybook-with-controls"> @@ -290,7 +316,15 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s }} > {Component ? ( - <Component {...filteredProps}>{children}</Component> + <> + {/* Show a trigger button for components like Modal */} + {triggerProp && ButtonComponent && ( + <ButtonComponent onClick={() => updateProp(triggerProp, true)}> + Open {component} + </ButtonComponent> + )} + <Component {...filteredProps} {...triggerProps}>{children}</Component> + </> ) : ( <div style={{ color: '#999' }}> Component "{String(component)}" not found @@ -389,7 +423,8 @@ function StoryWithControlsInner({ component, renderComponent, props, controls, s // A simple component to display a story with controls // renderComponent: optional override for which component to render (e.g., 'Icons.InfoCircleOutlined' when component='Icons') -export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle }) { +// triggerProp/onHideProp: for components like Modal that need a button to open (e.g., triggerProp="show", onHideProp="onHide") +export function StoryWithControls({ component: Component, renderComponent, props = {}, controls = [], sampleChildren, sampleChildrenStyle, triggerProp, onHideProp }) { return ( <BrowserOnly fallback={<LoadingPlaceholder />}> {() => ( @@ -400,6 +435,8 @@ export function StoryWithControls({ component: Component, renderComponent, props controls={controls} sampleChildren={sampleChildren} sampleChildrenStyle={sampleChildrenStyle} + triggerProp={triggerProp} + onHideProp={onHideProp} /> )} </BrowserOnly> diff --git a/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx index b7bf895a2f4..1d6671c6773 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Menu/Menu.stories.tsx @@ -21,6 +21,14 @@ import { Menu, MainNav } from '.'; export default { title: 'Components/Menu', component: Menu as React.FC, + parameters: { + docs: { + description: { + component: + 'Navigation menu component supporting horizontal, vertical, and inline modes. Based on Ant Design Menu with Superset styling.', + }, + }, + }, }; export const MainNavigation = (args: any) => ( @@ -47,18 +55,95 @@ export const InteractiveMenu = (args: any) => ( ); InteractiveMenu.args = { - defaultSelectedKeys: ['1'], - inlineCollapsed: false, mode: 'horizontal', - multiple: false, selectable: true, }; InteractiveMenu.argTypes = { mode: { - control: { - type: 'select', - }, + control: 'select', options: ['horizontal', 'vertical', 'inline'], + description: 'Menu display mode: horizontal navbar, vertical sidebar, or inline collapsible.', + }, + selectable: { + control: 'boolean', + description: 'Whether menu items can be selected.', + }, + multiple: { + control: 'boolean', + description: 'Allow multiple items to be selected.', + }, + inlineCollapsed: { + control: 'boolean', + description: 'Whether the inline menu is collapsed (only applies to inline mode).', + }, +}; + +InteractiveMenu.parameters = { + docs: { + staticProps: { + items: [ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { label: 'SQL Lab', key: 'sqllab' }, + ], + }, + liveExample: `function Demo() { + return ( + <Menu + mode="horizontal" + selectable + items={[ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { label: 'SQL Lab', key: 'sqllab' }, + ]} + /> + ); +}`, + examples: [ + { + title: 'Vertical Menu', + code: `function VerticalMenu() { + return ( + <Menu + mode="vertical" + style={{ width: 200 }} + items={[ + { label: 'Dashboards', key: 'dashboards' }, + { label: 'Charts', key: 'charts' }, + { label: 'Datasets', key: 'datasets' }, + { + label: 'Settings', + key: 'settings', + children: [ + { label: 'Profile', key: 'profile' }, + { label: 'Preferences', key: 'preferences' }, + ], + }, + ]} + /> + ); +}`, + }, + { + title: 'Menu with Icons', + code: `function MenuWithIcons() { + return ( + <Menu + mode="horizontal" + items={[ + { label: <><Icons.DashboardOutlined /> Dashboards</>, key: 'dashboards' }, + { label: <><Icons.LineChartOutlined /> Charts</>, key: 'charts' }, + { label: <><Icons.DatabaseOutlined /> Datasets</>, key: 'datasets' }, + { label: <><Icons.ConsoleSqlOutlined /> SQL Lab</>, key: 'sqllab' }, + ]} + /> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx index 0b9c855ca5b..f0083c04091 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Modal/Modal.stories.tsx @@ -24,6 +24,14 @@ import type { ModalProps, ModalFuncProps } from './types'; export default { title: 'Components/Modal', component: Modal, + parameters: { + docs: { + description: { + component: + 'Modal dialog component for displaying content that requires user attention or interaction. Supports customizable buttons, drag/resize, and confirmation dialogs.', + }, + }, + }, }; export const InteractiveModal = (props: ModalProps) => ( @@ -32,9 +40,9 @@ export const InteractiveModal = (props: ModalProps) => ( InteractiveModal.args = { disablePrimaryButton: false, - primaryButtonName: 'Danger', - primaryButtonStyle: 'danger', - show: true, + primaryButtonName: 'Submit', + primaryButtonStyle: 'primary', + show: false, title: "I'm a modal!", resizable: false, draggable: false, @@ -42,19 +50,118 @@ InteractiveModal.args = { }; InteractiveModal.argTypes = { + show: { + control: 'boolean', + description: 'Whether the modal is visible. Use the "Try It" example below for a working demo.', + }, + title: { + control: 'text', + description: 'Title displayed in the modal header.', + }, + primaryButtonName: { + control: 'text', + description: 'Text for the primary action button.', + }, primaryButtonStyle: { - description: 'The style of the primary action button.', + control: 'select', options: ['primary', 'secondary', 'dashed', 'danger', 'link'], - control: { type: 'select' }, + description: 'The style of the primary action button.', }, width: { - description: 'Width of the modal in pixels or as a string.', - control: { type: 'number' }, + control: 'number', + description: 'Width of the modal in pixels.', + }, + resizable: { + control: 'boolean', + description: 'Whether the modal can be resized by dragging corners.', + }, + draggable: { + control: 'boolean', + description: 'Whether the modal can be dragged by its header.', + }, + disablePrimaryButton: { + control: 'boolean', + description: 'Whether the primary button is disabled.', }, onHandledPrimaryAction: { action: 'onHandledPrimaryAction' }, onHide: { action: 'onHide' }, }; +InteractiveModal.parameters = { + docs: { + triggerProp: 'show', + onHideProp: 'onHide', + liveExample: `function ModalDemo() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + <Button onClick={() => setIsOpen(true)}>Open Modal</Button> + <Modal + show={isOpen} + onHide={() => setIsOpen(false)} + title="Example Modal" + primaryButtonName="Submit" + onHandledPrimaryAction={() => { + alert('Submitted!'); + setIsOpen(false); + }} + > + <p>This is the modal content. Click Submit or close the modal.</p> + </Modal> + </> + ); +}`, + examples: [ + { + title: 'Danger Modal', + code: `function DangerModal() { + const [isOpen, setIsOpen] = React.useState(false); + return ( + <> + <Button buttonStyle="danger" onClick={() => setIsOpen(true)}>Delete Item</Button> + <Modal + show={isOpen} + onHide={() => setIsOpen(false)} + title="Confirm Delete" + primaryButtonName="Delete" + primaryButtonStyle="danger" + onHandledPrimaryAction={() => { + alert('Deleted!'); + setIsOpen(false); + }} + > + <p>Are you sure you want to delete this item? This action cannot be undone.</p> + </Modal> + </> + ); +}`, + }, + { + title: 'Confirmation Dialogs', + code: `function ConfirmationDialogs() { + return ( + <div style={{ display: 'flex', gap: 8 }}> + <Button onClick={() => Modal.confirm({ + title: 'Confirm Action', + content: 'Are you sure you want to proceed?', + okText: 'Yes', + })}>Confirm</Button> + <Button onClick={() => Modal.warning({ + title: 'Warning', + content: 'This action may have consequences.', + })}>Warning</Button> + <Button onClick={() => Modal.error({ + title: 'Error', + content: 'Something went wrong.', + })}>Error</Button> + </div> + ); +}`, + }, + ], + }, +}; + export const ModalFunctions = (props: ModalFuncProps) => ( <div> <Button onClick={() => Modal.error(props)}>Error</Button> diff --git a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx index 739cfecc87a..3fd8bad4bfd 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/ModalTrigger.stories.tsx @@ -39,6 +39,14 @@ interface IModalTriggerProps { export default { title: 'Components/ModalTrigger', component: ModalTrigger, + parameters: { + docs: { + description: { + component: + 'A component that renders a trigger element which opens a modal when clicked. Useful for actions that need confirmation or additional input.', + }, + }, + }, }; export const InteractiveModalTrigger = (args: IModalTriggerProps) => ( @@ -47,13 +55,116 @@ export const InteractiveModalTrigger = (args: IModalTriggerProps) => ( InteractiveModalTrigger.args = { isButton: true, - modalTitle: 'I am a modal title', - modalBody: 'I am a modal body', - modalFooter: 'I am a modal footer', - tooltip: 'I am a tooltip', + modalTitle: 'Modal Title', + modalBody: 'This is the modal body content.', + tooltip: 'Click to open modal', width: '600px', maxWidth: '1000px', responsive: true, draggable: false, resizable: false, }; + +InteractiveModalTrigger.argTypes = { + triggerNode: { + control: false, + description: 'The clickable element that opens the modal when clicked.', + }, + isButton: { + control: 'boolean', + description: 'Whether to wrap the trigger in a button element.', + }, + modalTitle: { + control: 'text', + description: 'Title displayed in the modal header.', + }, + modalBody: { + control: 'text', + description: 'Content displayed in the modal body.', + }, + tooltip: { + control: 'text', + description: 'Tooltip text shown on hover over the trigger.', + }, + width: { + control: 'text', + description: 'Width of the modal (e.g., "600px", "80%").', + }, + maxWidth: { + control: 'text', + description: 'Maximum width of the modal.', + }, + responsive: { + control: 'boolean', + description: 'Whether the modal should be responsive.', + }, + draggable: { + control: 'boolean', + description: 'Whether the modal can be dragged by its header.', + }, + resizable: { + control: 'boolean', + description: 'Whether the modal can be resized by dragging corners.', + }, +}; + +InteractiveModalTrigger.parameters = { + docs: { + // Use a simple span for triggerNode since isButton: true wraps it in a button + staticProps: { + triggerNode: 'Click to Open Modal', + }, + liveExample: `function Demo() { + return ( + <ModalTrigger + isButton + triggerNode={<span>Click to Open</span>} + modalTitle="Example Modal" + modalBody={<p>This is the modal content. You can put any React elements here.</p>} + width="500px" + responsive + /> + ); +}`, + examples: [ + { + title: 'With Custom Trigger', + code: `function CustomTrigger() { + return ( + <ModalTrigger + triggerNode={ + <Button buttonStyle="primary"> + <Icons.PlusOutlined /> Add New Item + </Button> + } + modalTitle="Add New Item" + modalBody={ + <div> + <p>Fill out the form to add a new item.</p> + <Input placeholder="Item name" /> + </div> + } + width="400px" + /> + ); +}`, + }, + { + title: 'Draggable & Resizable', + code: `function DraggableModal() { + return ( + <ModalTrigger + isButton + triggerNode={<span>Open Draggable Modal</span>} + modalTitle="Draggable & Resizable" + modalBody={<p>Try dragging the header or resizing from the corners!</p>} + draggable + resizable + width="500px" + /> + ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx index c7d58485ece..0475286afb2 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Popover/Popover.stories.tsx @@ -22,6 +22,14 @@ import { Button } from '../Button'; export default { title: 'Components/Popover', component: Popover, + parameters: { + docs: { + description: { + component: + 'A floating card that appears when hovering or clicking a trigger element. Supports configurable placement, trigger behavior, and custom content.', + }, + }, + }, }; export const InteractivePopover = (args: PopoverProps) => ( @@ -37,31 +45,6 @@ export const InteractivePopover = (args: PopoverProps) => ( </Popover> ); -const PLACEMENTS = { - label: 'placement', - options: [ - 'topLeft', - 'top', - 'topRight', - 'leftTop', - 'left', - 'leftBottom', - 'rightTop', - 'right', - 'rightBottom', - 'bottomLeft', - 'bottom', - 'bottomRight', - ], - defaultValue: null, -}; - -const TRIGGERS = { - label: 'trigger', - options: ['hover', 'click', 'focus'], - defaultValue: null, -}; - InteractivePopover.args = { content: 'Popover sample content', title: 'Popover title', @@ -70,24 +53,118 @@ InteractivePopover.args = { }; InteractivePopover.argTypes = { + content: { + control: 'text', + description: 'Content displayed inside the popover body.', + }, + title: { + control: 'text', + description: 'Title displayed in the popover header.', + }, placement: { - name: PLACEMENTS.label, control: { type: 'select' }, - options: PLACEMENTS.options, + options: [ + 'topLeft', + 'top', + 'topRight', + 'leftTop', + 'left', + 'leftBottom', + 'rightTop', + 'right', + 'rightBottom', + 'bottomLeft', + 'bottom', + 'bottomRight', + ], + description: 'Position of the popover relative to the trigger element.', }, trigger: { - name: TRIGGERS.label, control: { type: 'select' }, - options: TRIGGERS.options, + options: ['hover', 'click', 'focus'], + description: 'Event that triggers the popover to appear.', }, arrow: { - name: 'arrow', control: { type: 'boolean' }, - description: "Change arrow's visible state", + description: "Whether to show the popover's arrow pointing to the trigger.", }, color: { - name: 'color', control: { type: 'color' }, description: 'The background color of the popover.', }, }; + +InteractivePopover.parameters = { + docs: { + sampleChildren: [ + { component: 'Button', props: { children: 'Hover me' } }, + ], + liveExample: `function Demo() { + return ( + <Popover + content="Popover sample content" + title="Popover title" + arrow + > + <Button>Hover me</Button> + </Popover> + ); +}`, + examples: [ + { + title: 'Click Trigger', + code: `function ClickPopover() { + return ( + <Popover + content="This popover appears on click." + title="Click Popover" + trigger="click" + > + <Button>Click me</Button> + </Popover> + ); +}`, + }, + { + title: 'Placements', + code: `function PlacementsDemo() { + return ( + <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', justifyContent: 'center', padding: '60px 0' }}> + {['top', 'right', 'bottom', 'left'].map(placement => ( + <Popover + key={placement} + content={\`This popover is placed on the \${placement}\`} + title={placement} + placement={placement} + > + <Button>{placement}</Button> + </Popover> + ))} + </div> + ); +}`, + }, + { + title: 'Rich Content', + code: `function RichPopover() { + return ( + <Popover + title="Dashboard Info" + content={ + <div> + <p><strong>Created by:</strong> Admin</p> + <p><strong>Last modified:</strong> Jan 2025</p> + <p><strong>Charts:</strong> 12</p> + </div> + } + > + <Button buttonStyle="primary"> + <Icons.InfoCircleOutlined /> View Details + </Button> + </Popover> + ); +}`, + }, + ], + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx index b696b92c7e9..d056a992ad4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ProgressBar/ProgressBar.stories.tsx @@ -32,7 +32,7 @@ export default { }; export const InteractiveProgressBar = (args: ProgressBarProps) => ( - <ProgressBar {...args} type="line" /> + <ProgressBar {...args} /> ); export const InteractiveProgressCircle = (args: ProgressBarProps) => ( @@ -43,21 +43,30 @@ export const InteractiveProgressDashboard = (args: ProgressBarProps) => ( <ProgressBar {...args} type="dashboard" /> ); -const commonArgs = { - striped: true, - percent: 90, +InteractiveProgressBar.args = { + percent: 75, + status: 'normal', + type: 'line', + striped: false, showInfo: true, - strokeColor: '#FF0000', - trailColor: '#000', strokeLinecap: 'round', - type: 'line', }; -const commonArgTypes = { +InteractiveProgressBar.argTypes = { percent: { control: { type: 'number', min: 0, max: 100 }, description: 'Completion percentage (0-100).', }, + status: { + control: 'select', + options: ['normal', 'success', 'exception', 'active'], + description: 'Current status of the progress bar.', + }, + type: { + control: 'select', + options: ['line', 'circle', 'dashboard'], + description: 'Display type: line, circle, or dashboard gauge.', + }, striped: { control: 'boolean', description: 'Whether to show striped animation on the bar.', @@ -68,7 +77,7 @@ const commonArgTypes = { }, strokeColor: { control: 'color', - description: 'Color of the progress bar.', + description: 'Color of the progress bar fill.', }, trailColor: { control: 'color', @@ -79,29 +88,20 @@ const commonArgTypes = { options: ['round', 'butt', 'square'], description: 'Shape of the progress bar endpoints.', }, - type: { - control: 'select', - options: ['line', 'circle', 'dashboard'], - description: 'Display type: line, circle, or dashboard gauge.', - }, -}; - -InteractiveProgressBar.args = { - ...commonArgs, - status: 'normal', -}; - -InteractiveProgressBar.argTypes = { - ...commonArgTypes, - status: { - control: 'select', - options: ['normal', 'success', 'exception', 'active'], - description: 'Current status of the progress bar.', - }, }; InteractiveProgressBar.parameters = { docs: { + liveExample: `function Demo() { + return ( + <ProgressBar + percent={75} + status="normal" + type="line" + showInfo + /> + ); +}`, examples: [ { title: 'All Progress Types', @@ -138,16 +138,70 @@ InteractiveProgressBar.parameters = { ))} </div> ); +}`, + }, + { + title: 'Custom Colors', + code: `function CustomColors() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> + <ProgressBar percent={50} strokeColor="#1890ff" /> + <ProgressBar percent={70} strokeColor="#52c41a" /> + <ProgressBar percent={30} strokeColor="#faad14" trailColor="#f0f0f0" /> + <ProgressBar percent={90} strokeColor="#ff4d4f" /> + </div> + ); }`, }, ], }, }; -InteractiveProgressCircle.args = commonArgs; +const commonArgs = { + striped: true, + percent: 90, + showInfo: true, + strokeColor: '#FF0000', + trailColor: '#000', + strokeLinecap: 'round', + type: 'line', +}; +const commonArgTypes = { + percent: { + control: { type: 'number', min: 0, max: 100 }, + description: 'Completion percentage (0-100).', + }, + striped: { + control: 'boolean', + description: 'Whether to show striped animation on the bar.', + }, + showInfo: { + control: 'boolean', + description: 'Whether to show the percentage text.', + }, + strokeColor: { + control: 'color', + description: 'Color of the progress bar.', + }, + trailColor: { + control: 'color', + description: 'Color of the unfilled portion.', + }, + strokeLinecap: { + control: 'select', + options: ['round', 'butt', 'square'], + description: 'Shape of the progress bar endpoints.', + }, + type: { + control: 'select', + options: ['line', 'circle', 'dashboard'], + description: 'Display type: line, circle, or dashboard gauge.', + }, +}; + +InteractiveProgressCircle.args = commonArgs; InteractiveProgressCircle.argTypes = commonArgTypes; InteractiveProgressDashboard.args = commonArgs; - InteractiveProgressDashboard.argTypes = commonArgTypes; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx index 3323a487281..5d6f48b4ce3 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx @@ -16,14 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import { StoryObj } from '@storybook/react'; -import { noop } from 'lodash'; import { SelectOptionsType, SelectProps } from './types'; import { Select } from '.'; export default { title: 'Components/Select', component: Select, + parameters: { + docs: { + description: { + component: + 'A versatile select component supporting single and multi-select modes, search filtering, option creation, and both synchronous and asynchronous data sources.', + }, + }, + }, }; const DEFAULT_WIDTH = 200; @@ -88,145 +94,197 @@ const generateOptions = (opts: SelectOptionsType, count: number) => { return generated.slice(0, count); }; -export const InteractiveSelect: StoryObj = { - render: ({ - header, - options, - optionsCount, - ...args - }: SelectProps & { header: string; optionsCount: number }) => { - noop(header); - return ( - <div - style={{ - width: DEFAULT_WIDTH, - }} - > - <Select - {...args} - options={ - Array.isArray(options) - ? generateOptions(options, optionsCount) - : options - } - mode="multiple" - /> - </div> - ); +export const InteractiveSelect = (args: SelectProps) => ( + <div style={{ width: DEFAULT_WIDTH }}> + <Select + ariaLabel="interactive-select" + options={options} + {...args} + /> + </div> +); + +InteractiveSelect.args = { + mode: 'single', + placeholder: 'Select ...', + showSearch: true, + allowNewOptions: false, + allowClear: false, + allowSelectAll: true, + disabled: false, + invertSelection: false, + oneLine: false, + maxTagCount: 4, +}; + +InteractiveSelect.argTypes = { + mode: { + control: 'inline-radio', + options: ['single', 'multiple'], + description: 'Whether to allow selection of a single option or multiple.', }, - args: { - autoFocus: true, - allowNewOptions: false, - allowClear: false, - autoClearSearchValue: false, - allowSelectAll: true, - disabled: false, - header: 'none', - invertSelection: false, - labelInValue: true, - maxTagCount: 4, - mode: 'single', - oneLine: false, - options, - optionsCount: options.length, - optionFilterProps: ['value', 'label', 'custom'], - placeholder: 'Select ...', - showSearch: true, + placeholder: { + control: 'text', + description: 'Placeholder text when no option is selected.', }, - argTypes: { - options: { - description: `It defines the options of the Select. - The options can be static, an array of options. - The options can also be async, a promise that returns an array of options. - `, - }, - ariaLabel: { - description: `It adds the aria-label tag for accessibility standards. - Must be plain English and localized. - `, - }, - labelInValue: { - table: { - disable: true, - }, - }, - name: { - table: { - disable: true, - }, + showSearch: { + control: 'boolean', + description: 'Whether to show a search input for filtering.', + }, + allowNewOptions: { + control: 'boolean', + description: 'Whether users can create new options by typing a value not in the list.', + }, + allowClear: { + control: 'boolean', + description: 'Whether to show a clear button to reset the selection.', + }, + allowSelectAll: { + control: 'boolean', + description: 'Whether to show a "Select All" option in multiple mode.', + }, + disabled: { + control: 'boolean', + description: 'Whether the select is disabled.', + }, + invertSelection: { + control: 'boolean', + description: 'Shows a stop icon instead of a checkmark on selected options, indicating deselection on click.', + }, + oneLine: { + control: 'boolean', + description: 'Forces tags onto one line with overflow count. Requires multiple mode.', + }, + maxTagCount: { + control: { type: 'number' }, + description: 'Maximum number of tags to display in multiple mode before showing an overflow count.', + }, +}; + +InteractiveSelect.parameters = { + docs: { + staticProps: { + options: [ + { label: 'Such an incredibly awesome long long label', value: 'long-label-1' }, + { label: 'Another incredibly awesome long long label', value: 'long-label-2' }, + { label: 'Option A', value: 'A' }, + { label: 'Option B', value: 'B' }, + { label: 'Option C', value: 'C' }, + { label: 'Option D', value: 'D' }, + { label: 'Option E', value: 'E' }, + { label: 'Option F', value: 'F' }, + { label: 'Option G', value: 'G' }, + { label: 'Option H', value: 'H' }, + { label: 'Option I', value: 'I' }, + ], }, - notFoundContent: { - table: { - disable: true, + liveExample: `function Demo() { + return ( + <div style={{ width: 300 }}> + <Select + ariaLabel="demo-select" + options={[ + { label: 'Dashboards', value: 'dashboards' }, + { label: 'Charts', value: 'charts' }, + { label: 'Datasets', value: 'datasets' }, + { label: 'SQL Lab', value: 'sqllab' }, + { label: 'Settings', value: 'settings' }, + ]} + placeholder="Select ..." + showSearch + /> + </div> + ); +}`, + examples: [ + { + title: 'Multi Select', + code: `function MultiSelectDemo() { + return ( + <div style={{ width: 400 }}> + <Select + ariaLabel="multi-select" + mode="multiple" + options={[ + { label: 'Dashboards', value: 'dashboards' }, + { label: 'Charts', value: 'charts' }, + { label: 'Datasets', value: 'datasets' }, + { label: 'SQL Lab', value: 'sqllab' }, + { label: 'Settings', value: 'settings' }, + ]} + placeholder="Select items..." + allowSelectAll + maxTagCount={3} + /> + </div> + ); +}`, }, - }, - mappedMode: { - table: { - disable: true, + { + title: 'Allow New Options', + code: `function AllowNewDemo() { + return ( + <div style={{ width: 300 }}> + <Select + ariaLabel="allow-new-select" + mode="multiple" + options={[ + { label: 'Red', value: 'red' }, + { label: 'Green', value: 'green' }, + { label: 'Blue', value: 'blue' }, + ]} + placeholder="Type to add tags..." + allowNewOptions + showSearch + /> + </div> + ); +}`, }, - }, - mode: { - description: `It defines whether the Select should allow for - the selection of multiple options or single. Single by default. - `, - control: { - type: 'inline-radio', - options: ['single', 'multiple'], + { + title: 'Inverted Selection', + code: `function InvertedDemo() { + return ( + <div style={{ width: 400 }}> + <Select + ariaLabel="inverted-select" + mode="multiple" + options={[ + { label: 'Admin', value: 'admin' }, + { label: 'Editor', value: 'editor' }, + { label: 'Viewer', value: 'viewer' }, + { label: 'Public', value: 'public' }, + ]} + placeholder="Exclude roles..." + invertSelection + /> + </div> + ); +}`, }, - }, - allowNewOptions: { - description: `It enables the user to create new options. - Can be used with standard or async select types. - Can be used with any mode, single or multiple. False by default. - `, - }, - invertSelection: { - description: `It shows a stop-outlined icon at the far right of a selected - option instead of the default checkmark. - Useful to better indicate to the user that by clicking on a selected - option it will be de-selected. False by default. - `, - }, - optionFilterProps: { - description: `It allows to define which properties of the option object - should be looked for when searching. - By default label and value. - `, - }, - oneLine: { - description: `Sets maxTagCount to 1. The overflow tag is always displayed in - the same line, line wrapping is disabled. - When the dropdown is open, sets maxTagCount to 0, - displays only the overflow tag. - Requires '"mode=multiple"'. - `, - }, - maxTagCount: { - description: `Sets maxTagCount attribute. The overflow tag is displayed in - place of the remaining items. - Requires '"mode=multiple"'. - `, - }, - optionsCount: { - control: { - type: 'number', + { + title: 'One Line Mode', + code: `function OneLineDemo() { + return ( + <div style={{ width: 300 }}> + <Select + ariaLabel="oneline-select" + mode="multiple" + options={[ + { label: 'Dashboard 1', value: 'd1' }, + { label: 'Dashboard 2', value: 'd2' }, + { label: 'Dashboard 3', value: 'd3' }, + { label: 'Dashboard 4', value: 'd4' }, + { label: 'Dashboard 5', value: 'd5' }, + ]} + placeholder="Select dashboards..." + oneLine + /> + </div> + ); +}`, }, - }, - header: { - description: `It adds a header on top of the Select. Can be any ReactNode.`, - control: { type: 'inline-radio', options: ['none', 'text', 'control'] }, - }, - pageSize: { - description: `It defines how many results should be included in the query response. - Works in async mode only (See the options property). - `, - }, - fetchOnlyOnSearch: { - description: `It fires a request against the server only after searching. - Works in async mode only (See the options property). - Undefined by default. - `, - }, + ], }, }; @@ -310,3 +368,49 @@ PageScroll.parameters = { disable: true, }, }; + +/** + * Extended interactive story for Storybook with additional controls + * (header, optionsCount, generateOptions) that are story-specific utilities. + */ +export const AdvancedPlayground = ( + args: SelectProps & { optionsCount: number }, +) => { + const { optionsCount, ...selectArgs } = args; + return ( + <div style={{ width: DEFAULT_WIDTH }}> + <Select + {...selectArgs} + options={generateOptions(options, optionsCount)} + /> + </div> + ); +}; + +AdvancedPlayground.args = { + autoFocus: true, + allowNewOptions: false, + allowClear: false, + autoClearSearchValue: false, + allowSelectAll: true, + disabled: false, + invertSelection: false, + labelInValue: true, + maxTagCount: 4, + mode: 'multiple', + oneLine: false, + optionsCount: options.length, + optionFilterProps: ['value', 'label', 'custom'], + placeholder: 'Select ...', + showSearch: true, +}; + +AdvancedPlayground.argTypes = { + mode: { + control: { type: 'inline-radio' }, + options: ['single', 'multiple'], + }, + optionsCount: { + control: { type: 'number' }, + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx index 121f1fa0484..270805fa425 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Slider/Slider.stories.tsx @@ -21,6 +21,14 @@ import Slider, { SliderSingleProps, SliderRangeProps } from '.'; export default { title: 'Components/Slider', component: Slider, + parameters: { + docs: { + description: { + component: + 'A slider input for selecting a value or range from a continuous or stepped interval. Supports single value, range, vertical orientation, marks, and tooltip display.', + }, + }, + }, }; const tooltipPlacement = [ @@ -75,33 +83,181 @@ InteractiveSlider.args = { max: 100, defaultValue: 70, step: 1, - marks: {}, disabled: false, reverse: false, vertical: false, - autoFocus: false, keyboard: true, dots: false, included: true, - tooltipPosition: 'bottom', }; InteractiveSlider.argTypes = { - onChange: { action: 'onChange' }, - onChangeComplete: { action: 'onChangeComplete' }, + min: { + control: { type: 'number' }, + description: 'Minimum value of the slider.', + }, + max: { + control: { type: 'number' }, + description: 'Maximum value of the slider.', + }, + defaultValue: { + control: { type: 'number' }, + description: 'Initial value of the slider.', + }, + step: { + control: { type: 'number' }, + description: 'Step increment between values. Use null for marks-only mode.', + }, + disabled: { + control: 'boolean', + description: 'Whether the slider is disabled.', + }, + reverse: { + control: 'boolean', + description: 'Whether to reverse the slider direction.', + }, + vertical: { + control: 'boolean', + description: 'Whether to display the slider vertically.', + }, + keyboard: { + control: 'boolean', + description: 'Whether keyboard arrow keys can control the slider.', + }, + dots: { + control: 'boolean', + description: 'Whether to show dots at each step mark.', + }, + included: { + control: 'boolean', + description: 'Whether to highlight the filled portion of the track.', + }, tooltipOpen: { - control: { type: 'boolean' }, + control: 'boolean', + description: 'Whether the value tooltip is always visible.', }, tooltipPosition: { - options: tooltipPlacement, control: { type: 'select' }, + options: [ + 'top', + 'left', + 'bottom', + 'right', + 'topLeft', + 'topRight', + 'bottomLeft', + 'bottomRight', + 'leftTop', + 'leftBottom', + 'rightTop', + 'rightBottom', + ], + description: 'Position of the value tooltip relative to the handle.', + }, + onChange: { action: 'onChange' }, + onChangeComplete: { action: 'onChangeComplete' }, +}; + +InteractiveSlider.parameters = { + docs: { + liveExample: `function Demo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <Slider + min={0} + max={100} + defaultValue={70} + step={1} + /> + </div> + ); +}`, + examples: [ + { + title: 'Range Slider', + code: `function RangeSliderDemo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <h4>Basic Range</h4> + <Slider range defaultValue={[20, 70]} min={0} max={100} /> + <br /> + <h4>Draggable Track</h4> + <Slider range={{ draggableTrack: true }} defaultValue={[30, 60]} min={0} max={100} /> + </div> + ); +}`, + }, + { + title: 'With Marks', + code: `function MarksDemo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <Slider + min={0} + max={100} + defaultValue={37} + marks={{ + 0: '0°C', + 25: '25°C', + 50: '50°C', + 75: '75°C', + 100: '100°C', + }} + /> + </div> + ); +}`, + }, + { + title: 'Stepped and Dots', + code: `function SteppedDemo() { + return ( + <div style={{ width: 400, padding: '20px 0' }}> + <h4>Step = 10 with Dots</h4> + <Slider min={0} max={100} defaultValue={30} step={10} dots /> + <br /> + <h4>Step = 25</h4> + <Slider min={0} max={100} defaultValue={50} step={25} dots + marks={{ 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }} /> + </div> + ); +}`, + }, + { + title: 'Vertical Slider', + code: `function VerticalDemo() { + return ( + <div style={{ height: 300, display: 'flex', gap: 40, padding: '0 40px' }}> + <Slider vertical defaultValue={30} /> + <Slider vertical range defaultValue={[20, 60]} /> + <Slider vertical defaultValue={50} dots step={10} + marks={{ 0: '0', 50: '50', 100: '100' }} /> + </div> + ); +}`, + }, + ], }, }; InteractiveRangeSlider.args = { - ...InteractiveSlider.args, + min: 0, + max: 100, defaultValue: [50, 70], + step: 1, + disabled: false, + reverse: false, + vertical: false, + keyboard: true, + dots: false, + included: true, draggableTrack: false, }; -InteractiveRangeSlider.argTypes = InteractiveSlider.argTypes; +InteractiveRangeSlider.argTypes = { + ...InteractiveSlider.argTypes, + draggableTrack: { + control: 'boolean', + description: 'Whether the track between handles can be dragged to move both handles together.', + }, +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx index 336c0ef0dd0..cfc0c75f5e2 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Steps/Steps.stories.tsx @@ -22,12 +22,21 @@ import { Steps, type StepsProps } from '.'; export default { title: 'Components/Steps', component: Steps as typeof AntdSteps, + parameters: { + docs: { + description: { + component: + 'A navigation component for guiding users through multi-step workflows. Supports horizontal, vertical, and inline layouts with progress tracking.', + }, + }, + }, }; export const InteractiveSteps = (args: StepsProps) => <Steps {...args} />; + InteractiveSteps.args = { direction: 'horizontal', - initial: 0, + current: 1, labelPlacement: 'horizontal', progressDot: false, size: 'default', @@ -51,23 +60,144 @@ InteractiveSteps.args = { InteractiveSteps.argTypes = { direction: { - options: ['horizontal', 'vertical'], control: { type: 'select' }, + options: ['horizontal', 'vertical'], + description: 'Layout direction of the steps.', + }, + current: { + control: { type: 'number' }, + description: 'Index of the current step (zero-based).', }, labelPlacement: { - options: ['horizontal', 'vertical'], control: { type: 'select' }, + options: ['horizontal', 'vertical'], + description: 'Position of step labels relative to the step icon.', + }, + progressDot: { + control: 'boolean', + description: 'Whether to use a dot style instead of numbered icons.', }, size: { - options: ['default', 'small'], control: { type: 'select' }, + options: ['default', 'small'], + description: 'Size of the step icons and text.', }, status: { - options: ['wait', 'process', 'finish', 'error'], control: { type: 'select' }, + options: ['wait', 'process', 'finish', 'error'], + description: 'Status of the current step.', }, type: { - options: ['default', 'navigation', 'inline'], control: { type: 'select' }, + options: ['default', 'navigation', 'inline'], + description: 'Visual style: default numbered, navigation breadcrumb, or inline compact.', + }, +}; + +InteractiveSteps.parameters = { + docs: { + staticProps: { + items: [ + { title: 'Connect Database', description: 'Configure the connection' }, + { title: 'Create Dataset', description: 'Select tables and columns' }, + { title: 'Build Chart', description: 'Choose visualization type' }, + ], + }, + liveExample: `function Demo() { + return ( + <Steps + current={1} + items={[ + { title: 'Connect Database', description: 'Configure the connection' }, + { title: 'Create Dataset', description: 'Select tables and columns' }, + { title: 'Build Chart', description: 'Choose visualization type' }, + ]} + /> + ); +}`, + examples: [ + { + title: 'Vertical Steps', + code: `function VerticalSteps() { + return ( + <Steps + direction="vertical" + current={1} + items={[ + { title: 'Upload CSV', description: 'Select a file from your computer' }, + { title: 'Configure Columns', description: 'Set data types and names' }, + { title: 'Review', description: 'Verify the data looks correct' }, + { title: 'Import', description: 'Save the dataset' }, + ]} + /> + ); +}`, + }, + { + title: 'Status Indicators', + code: `function StatusSteps() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> + <div> + <h4>Error on Step 2</h4> + <Steps + current={1} + status="error" + items={[ + { title: 'Connection', description: 'Configured' }, + { title: 'Validation', description: 'Failed to validate' }, + { title: 'Complete' }, + ]} + /> + </div> + <div> + <h4>All Complete</h4> + <Steps + current={3} + items={[ + { title: 'Step 1' }, + { title: 'Step 2' }, + { title: 'Step 3' }, + ]} + /> + </div> + </div> + ); +}`, + }, + { + title: 'Dot Style and Small Size', + code: `function DotAndSmall() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}> + <div> + <h4>Progress Dots</h4> + <Steps + progressDot + current={1} + items={[ + { title: 'Create', description: 'Define the resource' }, + { title: 'Configure', description: 'Set parameters' }, + { title: 'Deploy', description: 'Go live' }, + ]} + /> + </div> + <div> + <h4>Small Size</h4> + <Steps + size="small" + current={2} + items={[ + { title: 'Login' }, + { title: 'Verify' }, + { title: 'Done' }, + ]} + /> + </div> + </div> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx index 508549ac0e3..7fb89cc2ef9 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Switch/Switch.stories.tsx @@ -21,6 +21,14 @@ import { Switch, type SwitchProps } from '.'; export default { title: 'Components/Switch', + parameters: { + docs: { + description: { + component: + 'A toggle switch for boolean on/off states. Supports loading indicators, sizing, and an HTML title attribute for accessibility tooltips.', + }, + }, + }, }; export const InteractiveSwitch = ({ checked, ...rest }: SwitchProps) => { @@ -39,15 +47,120 @@ InteractiveSwitch.args = { checked: defaultCheckedValue, disabled: false, loading: false, - title: 'Switch', + title: 'Toggle feature', defaultChecked: defaultCheckedValue, - autoFocus: true, }; InteractiveSwitch.argTypes = { + checked: { + control: 'boolean', + description: 'Whether the switch is on.', + }, + disabled: { + control: 'boolean', + description: 'Whether the switch is disabled.', + }, + loading: { + control: 'boolean', + description: 'Whether to show a loading spinner inside the switch.', + }, + title: { + control: 'text', + description: 'HTML title attribute shown as a browser tooltip on hover. Useful for accessibility.', + }, size: { - defaultValue: 'default', control: { type: 'radio' }, options: ['small', 'default'], + description: 'Size of the switch.', + }, +}; + +InteractiveSwitch.parameters = { + docs: { + liveExample: `function Demo() { + const [checked, setChecked] = React.useState(true); + return ( + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={checked} + onChange={setChecked} + title="Toggle feature" + /> + <span>{checked ? 'On' : 'Off'}</span> + <span style={{ color: '#999', fontSize: 12 }}>(hover the switch to see the title tooltip)</span> + </div> + ); +}`, + examples: [ + { + title: 'Switch States', + code: `function SwitchStates() { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch defaultChecked title="Enabled switch" /> + <span>Checked</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch title="Unchecked switch" /> + <span>Unchecked</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch disabled defaultChecked title="Disabled on" /> + <span>Disabled (on)</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch disabled title="Disabled off" /> + <span>Disabled (off)</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch loading defaultChecked title="Loading switch" /> + <span>Loading</span> + </div> + </div> + ); +}`, + }, + { + title: 'Sizes', + code: `function SizesDemo() { + return ( + <div style={{ display: 'flex', alignItems: 'center', gap: 24 }}> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch size="small" defaultChecked title="Small switch" /> + <span>Small</span> + </div> + <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch size="default" defaultChecked title="Default switch" /> + <span>Default</span> + </div> + </div> + ); +}`, + }, + { + title: 'Settings Panel', + code: `function SettingsPanel() { + const [notifications, setNotifications] = React.useState(true); + const [darkMode, setDarkMode] = React.useState(false); + const [autoRefresh, setAutoRefresh] = React.useState(true); + return ( + <div style={{ maxWidth: 320, border: '1px solid #e8e8e8', borderRadius: 8, padding: 16 }}> + <h4 style={{ marginTop: 0 }}>Dashboard Settings</h4> + {[ + { label: 'Email notifications', checked: notifications, onChange: setNotifications, title: 'Toggle email notifications' }, + { label: 'Dark mode', checked: darkMode, onChange: setDarkMode, title: 'Toggle dark mode' }, + { label: 'Auto-refresh data', checked: autoRefresh, onChange: setAutoRefresh, title: 'Toggle auto-refresh' }, + ].map(({ label, checked, onChange, title }) => ( + <div key={label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid #f0f0f0' }}> + <span>{label}</span> + <Switch checked={checked} onChange={onChange} title={title} /> + </div> + ))} + </div> + ); +}`, + }, + ], }, }; diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx index 08c131060e1..509b4956d66 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.stories.tsx @@ -21,6 +21,14 @@ import { TableView, TableViewProps, EmptyWrapperType } from '.'; export default { title: 'Components/TableView', component: TableView, + parameters: { + docs: { + description: { + component: + 'A data table component with sorting, pagination, text wrapping, and empty state support. Built on react-table.', + }, + }, + }, }; export const InteractiveTableView = (args: TableViewProps) => ( @@ -67,7 +75,7 @@ InteractiveTableView.args = { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam id porta neque, a vehicula orci. Maecenas rhoncus elit sit amet purus convallis placerat in at nunc. Nulla nec viverra augue.', }, { - id: 321, + id: 456, age: 10, name: 'John Smith', summary: @@ -76,7 +84,7 @@ InteractiveTableView.args = { ], initialSortBy: [{ id: 'name', desc: true }], noDataText: 'No data here', - pageSize: 1, + pageSize: 2, showRowCount: true, withPagination: true, columnsForWrapText: ['Summary'], @@ -84,22 +92,131 @@ InteractiveTableView.args = { }; InteractiveTableView.argTypes = { + pageSize: { + control: { type: 'number', min: 1 }, + description: 'Number of rows displayed per page.', + }, + withPagination: { + control: 'boolean', + description: 'Whether to show pagination controls below the table.', + }, + showRowCount: { + control: 'boolean', + description: 'Whether to display the total row count alongside pagination.', + }, + noDataText: { + control: 'text', + description: 'Text displayed when the table has no data.', + }, + scrollTopOnPagination: { + control: 'boolean', + description: 'Whether to scroll to the top of the table when changing pages.', + }, emptyWrapperType: { - control: { - type: 'select', - }, + control: { type: 'select' }, options: [EmptyWrapperType.Default, EmptyWrapperType.Small], - }, - pageSize: { - control: { - type: 'number', - min: 1, - }, + description: 'Style of the empty state wrapper.', }, initialPageIndex: { - control: { - type: 'number', - min: 0, + control: { type: 'number', min: 0 }, + description: 'Initial page to display (zero-based).', + }, +}; + +InteractiveTableView.parameters = { + docs: { + staticProps: { + columns: [ + { accessor: 'id', Header: 'ID', sortable: true, id: 'id' }, + { accessor: 'age', Header: 'Age', id: 'age' }, + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'summary', Header: 'Summary', id: 'summary' }, + ], + data: [ + { id: 123, age: 27, name: 'Emily', summary: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' }, + { id: 321, age: 10, name: 'Kate', summary: 'Nam id porta neque, a vehicula orci.' }, + { id: 456, age: 10, name: 'John Smith', summary: 'Maecenas rhoncus elit sit amet purus convallis placerat.' }, + ], }, + liveExample: `function Demo() { + return ( + <TableView + columns={[ + { accessor: 'id', Header: 'ID', sortable: true, id: 'id' }, + { accessor: 'age', Header: 'Age', id: 'age' }, + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'summary', Header: 'Summary', id: 'summary' }, + ]} + data={[ + { id: 123, age: 27, name: 'Emily', summary: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' }, + { id: 321, age: 10, name: 'Kate', summary: 'Nam id porta neque, a vehicula orci.' }, + { id: 456, age: 10, name: 'John Smith', summary: 'Maecenas rhoncus elit sit amet purus convallis placerat.' }, + ]} + initialSortBy={[{ id: 'name', desc: true }]} + pageSize={2} + withPagination + showRowCount + /> + ); +}`, + examples: [ + { + title: 'Without Pagination', + code: `function NoPaginationDemo() { + return ( + <TableView + columns={[ + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'email', Header: 'Email', id: 'email' }, + { accessor: 'status', Header: 'Status', id: 'status' }, + ]} + data={[ + { name: 'Alice', email: '[email protected]', status: 'Active' }, + { name: 'Bob', email: '[email protected]', status: 'Inactive' }, + { name: 'Charlie', email: '[email protected]', status: 'Active' }, + ]} + withPagination={false} + /> + ); +}`, + }, + { + title: 'Empty State', + code: `function EmptyDemo() { + return ( + <TableView + columns={[ + { accessor: 'name', Header: 'Name', id: 'name' }, + { accessor: 'value', Header: 'Value', id: 'value' }, + ]} + data={[]} + noDataText="No results found" + /> + ); +}`, + }, + { + title: 'With Sorting', + code: `function SortingDemo() { + return ( + <TableView + columns={[ + { accessor: 'id', Header: 'ID', id: 'id', sortable: true }, + { accessor: 'name', Header: 'Name', id: 'name', sortable: true }, + { accessor: 'score', Header: 'Score', id: 'score', sortable: true }, + ]} + data={[ + { id: 1, name: 'Dashboard A', score: 95 }, + { id: 2, name: 'Dashboard B', score: 72 }, + { id: 3, name: 'Dashboard C', score: 88 }, + { id: 4, name: 'Dashboard D', score: 64 }, + ]} + initialSortBy={[{ id: 'score', desc: true }]} + withPagination={false} + /> + ); +}`, + }, + ], }, };
