diff --git a/.changeset/healthy-toads-rollerblade.md b/.changeset/healthy-toads-rollerblade.md
new file mode 100644
index 00000000000..baa86a1d133
--- /dev/null
+++ b/.changeset/healthy-toads-rollerblade.md
@@ -0,0 +1,5 @@
+---
+'@shopify/polaris': minor
+---
+
+Added support for setting custom markup on the `ContextualSaveBar` `saveAction` prop
diff --git a/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.module.scss b/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.module.scss
index 62b7c2a663f..b92ee7d83d5 100644
--- a/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.module.scss
+++ b/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.module.scss
@@ -1,12 +1,6 @@
@import '../../../../styles/common';
.ContextualSaveBar {
- /* stylelint-disable -- polaris: Used to apply dark theme to action buttons */
- --p-color-bg-surface: var(--p-color-bg-inverse);
- --p-color-text: var(--p-color-text-inverse);
- --p-color-bg-surface-hover: var(--p-color-bg-fill-inverse-hover);
- --p-color-bg-surface-secondary-active: var(--p-color-bg-fill-inverse-active);
- /* stylelint-enable */
display: flex;
height: $top-bar-height;
background: var(--p-color-bg-inverse);
@@ -95,8 +89,8 @@
--pc-button-bg_pressed: rgba(247, 247, 247, 1);
--pc-button-bg_disabled: rgba(255, 255, 255, 0.2);
--pc-button-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.48) inset,
- -1px 0 0 0 rgba(255, 255, 255, 0.20) inset,
- 1px 0 0 0 rgba(255, 255, 255, 0.20) inset,
+ -1px 0 0 0 rgba(255, 255, 255, 0.2) inset,
+ 1px 0 0 0 rgba(255, 255, 255, 0.2) inset,
0 -1.5px 0 0 rgba(0, 0, 0, 0.25) inset;
--pc-button-box-shadow_active: 0px 2px 1px 0px rgba(26, 26, 26, 0.2) inset,
1px 0px 1px 0px rgba(26, 26, 26, 0.12) inset,
diff --git a/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx b/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx
index 0ed2fbcf44f..fe0276c320f 100644
--- a/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx
+++ b/polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx
@@ -3,16 +3,19 @@ import {AlertTriangleIcon} from '@shopify/polaris-icons';
import {Button} from '../../../Button';
import {Image} from '../../../Image';
-// eslint-disable-next-line import/no-deprecated
-import {LegacyStack} from '../../../LegacyStack';
+import {InlineStack} from '../../../InlineStack';
import {Text} from '../../../Text';
import {Icon} from '../../../Icon';
import {classNames} from '../../../../utilities/css';
+import type {
+ ContextualSaveBarProps,
+ ContextualSaveBarAction,
+} from '../../../../utilities/frame';
import {useFrame} from '../../../../utilities/frame';
-import type {ContextualSaveBarProps} from '../../../../utilities/frame';
import {getWidth} from '../../../../utilities/get-width';
import {useI18n} from '../../../../utilities/i18n';
import {useToggle} from '../../../../utilities/use-toggle';
+import {isInterface} from '../../../../utilities/is-interface';
import {DiscardConfirmationModal} from './components';
import styles from './ContextualSaveBar.module.scss';
@@ -77,25 +80,34 @@ export function ContextualSaveBar({
);
+ let saveActionMarkup;
+
const saveActionContent =
- saveAction && saveAction.content
+ saveAction && 'content' in saveAction
? saveAction.content
: i18n.translate('Polaris.ContextualSaveBar.save');
- const saveActionMarkup = saveAction && (
-
- );
+ if (saveAction && isInterface(saveAction)) {
+ const {url, loading, disabled, onAction} =
+ saveAction as ContextualSaveBarAction;
+
+ saveActionMarkup = (
+
+ );
+ } else {
+ saveActionMarkup = saveAction;
+ }
const width = getWidth(logo, 104);
@@ -134,11 +146,11 @@ export function ContextualSaveBar({
)}
-
+
{secondaryMenu}
{discardActionMarkup}
{saveActionMarkup}
-
+
diff --git a/polaris-react/src/components/Page/Page.stories.tsx b/polaris-react/src/components/Page/Page.stories.tsx
index de4048e0085..4cf0d79ab9c 100644
--- a/polaris-react/src/components/Page/Page.stories.tsx
+++ b/polaris-react/src/components/Page/Page.stories.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useState, useRef} from 'react';
import type {ComponentMeta} from '@storybook/react';
import {
DeleteIcon,
@@ -7,6 +7,7 @@ import {
ExternalIcon,
ViewIcon,
MenuVerticalIcon,
+ ChevronDownIcon,
} from '@shopify/polaris-icons';
import {
Badge,
@@ -15,6 +16,13 @@ import {
LegacyStack,
Page,
PageActions,
+ Popover,
+ ActionList,
+ ButtonGroup,
+ TextField,
+ FormLayout,
+ ContextualSaveBar,
+ Frame,
} from '@shopify/polaris';
export default {
@@ -493,3 +501,177 @@ export function WithContentAfterTitleAndSubtitle() {
);
}
+
+export function WithSplitSaveAction() {
+ const initialState = {
+ title: 'Jar With Lock-Lid',
+ description: '',
+ isDraft: false,
+ };
+
+ const [active, setActive] = React.useState(false);
+ const [title, setTitle] = useState('Jar With Lock-Lid');
+ const [description, setDescription] = useState('');
+ const [isDirty, setIsDirty] = useState(false);
+ const [isDraft, setIsDraft] = useState(false);
+
+ const savedEditHistory = useRef<
+ {
+ title: string;
+ description: string;
+ isDraft: boolean;
+ }[]
+ >([]);
+
+ const handleChange = (name: string) => (value: string) => {
+ switch (name) {
+ case 'title':
+ handleDirtyState(name, value, title);
+ setTitle(value);
+ break;
+ case 'description':
+ handleDirtyState(name, value, description);
+ setDescription(value);
+ break;
+ default:
+ null;
+ }
+ };
+
+ const handleDirtyState = (
+ name: string,
+ newValue: string,
+ currentValue: string,
+ ) => {
+ if (
+ (newValue !== initialState[name] && !isDirty) ||
+ newValue !== currentValue
+ ) {
+ setIsDirty(true);
+ } else {
+ setIsDirty(false);
+ }
+ };
+
+ const handleDiscard = () => {
+ const previousState: {
+ title: string;
+ description: string;
+ isDraft: boolean;
+ } = savedEditHistory.current.pop() ?? initialState;
+
+ setTitle(previousState.title);
+ setDescription(previousState.description);
+ setIsDraft(previousState.isDraft);
+ setIsDirty(false);
+ };
+
+ const splitButton = (
+
+
+
+ setActive(true)}
+ icon={ChevronDownIcon}
+ accessibilityLabel="Other save actions"
+ />
+ }
+ autofocusTarget="first-node"
+ onClose={() => setActive(false)}
+ zIndexOverride={514}
+ >
+ {
+ setIsDraft(true);
+ savedEditHistory.current.push({
+ title,
+ description,
+ isDraft: true,
+ });
+ },
+ },
+ ]}
+ onActionAnyItem={() => setIsDirty(false)}
+ />
+
+
+ );
+
+ const saveBar = isDirty ? (
+
+ ) : null;
+
+ console.log(savedEditHistory);
+
+ return (
+
+ {saveBar}
+
+ {isDraft ? 'Draft' : 'Active'}
+
+ }
+ secondaryActions={[
+ {content: 'Duplicate'},
+ {content: 'View on your store'},
+ ]}
+ pagination={{
+ hasPrevious: true,
+ hasNext: true,
+ }}
+ >
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/polaris-react/src/utilities/frame/index.ts b/polaris-react/src/utilities/frame/index.ts
index e567a36650f..a71a1a73312 100644
--- a/polaris-react/src/utilities/frame/index.ts
+++ b/polaris-react/src/utilities/frame/index.ts
@@ -3,6 +3,7 @@ export * from './hooks';
export * from './context';
export type {
+ ContextualSaveBarAction,
ContextualSaveBarProps,
ToastProps,
ToastID,
diff --git a/polaris-react/src/utilities/frame/types.ts b/polaris-react/src/utilities/frame/types.ts
index 8af08faa5de..ff4e2b9a99a 100644
--- a/polaris-react/src/utilities/frame/types.ts
+++ b/polaris-react/src/utilities/frame/types.ts
@@ -13,7 +13,7 @@ export interface Logo {
width?: number;
}
-interface ContextualSaveBarAction {
+export interface ContextualSaveBarAction {
/** A destination to link to */
url?: string;
/** Content the action displays */
@@ -40,7 +40,7 @@ export interface ContextualSaveBarProps {
/** Accepts a string of content that will be rendered to the left of the actions */
message?: string;
/** Save or commit contextual save bar action with text defaulting to 'Save' */
- saveAction?: ContextualSaveBarAction;
+ saveAction?: ContextualSaveBarAction | React.JSX.Element;
/** Discard or cancel contextual save bar action with text defaulting to 'Discard' */
discardAction?: ContextualSaveBarCombinedActionProps;
/** Remove the normal max-width on the contextual save bar */
diff --git a/polaris.shopify.com/pages/examples/page-with-custom-primary-action.tsx b/polaris.shopify.com/pages/examples/page-with-custom-primary-action.tsx
index ef1816ff8e9..1aee7beac87 100644
--- a/polaris.shopify.com/pages/examples/page-with-custom-primary-action.tsx
+++ b/polaris.shopify.com/pages/examples/page-with-custom-primary-action.tsx
@@ -1,18 +1,197 @@
-import {Page, Button, LegacyCard} from '@shopify/polaris';
-import React from 'react';
+import {
+ Page,
+ Badge,
+ LegacyCard,
+ TextField,
+ Button,
+ ButtonGroup,
+ Popover,
+ Frame,
+ ContextualSaveBar,
+ FormLayout,
+ ActionList,
+} from '@shopify/polaris';
+import React, {useState, useRef} from 'react';
import {withPolarisExample} from '../../src/components/PolarisExampleWrapper';
+import {ChevronDownIcon} from '@shopify/polaris-icons';
function PageExample() {
+ interface FormState {
+ title: string;
+ description: string;
+ isDraft: boolean;
+ }
+
+ const initialState: FormState = {
+ title: 'Jar With Lock-Lid',
+ description: '',
+ isDraft: false,
+ };
+
+ const [active, setActive] = React.useState(false);
+ const [title, setTitle] = useState('Jar With Lock-Lid');
+ const [description, setDescription] = useState('');
+ const [isDirty, setIsDirty] = useState(false);
+ const [isDraft, setIsDraft] = useState(false);
+
+ const savedEditHistory = useRef<
+ {
+ title: string;
+ description: string;
+ isDraft: boolean;
+ }[]
+ >([]);
+
+ const handleChange = (name: string) => (value: string) => {
+ switch (name) {
+ case 'title':
+ handleDirtyState(name, value, title);
+ setTitle(value);
+ break;
+ case 'description':
+ handleDirtyState(name, value, description);
+ setDescription(value);
+ break;
+ default:
+ null;
+ }
+ };
+
+ const handleDirtyState = (
+ name: keyof FormState,
+ newValue: string,
+ currentValue: string,
+ ) => {
+ if (
+ (newValue !== initialState[name] && !isDirty) ||
+ newValue !== currentValue
+ ) {
+ setIsDirty(true);
+ } else {
+ setIsDirty(false);
+ }
+ };
+
+ const handleDiscard = () => {
+ const previousState: {
+ title: string;
+ description: string;
+ isDraft: boolean;
+ } = savedEditHistory.current.pop() ?? initialState;
+
+ setTitle(previousState?.title);
+ setDescription(previousState?.description);
+ setIsDraft(previousState?.isDraft);
+ setIsDirty(false);
+ };
+
+ const splitButton = (
+
+
+
+ setActive(true)}
+ icon={ChevronDownIcon}
+ accessibilityLabel="Other save actions"
+ />
+ }
+ autofocusTarget="first-node"
+ onClose={() => setActive(false)}
+ zIndexOverride={514}
+ >
+ {
+ setIsDraft(true);
+ savedEditHistory.current.push({
+ title,
+ description,
+ isDraft: true,
+ });
+ },
+ },
+ ]}
+ onActionAnyItem={() => setIsDirty(false)}
+ />
+
+
+ );
+
+ const saveBar = isDirty ? (
+
+ ) : null;
+
+ console.log(savedEditHistory);
+
return (
- Save}
+
-
- Credit card information
-
-
+ {saveBar}
+
+ {isDraft ? 'Draft' : 'Active'}
+
+ }
+ secondaryActions={[
+ {content: 'Duplicate'},
+ {content: 'View on your store'},
+ ]}
+ pagination={{
+ hasPrevious: true,
+ hasNext: true,
+ }}
+ >
+
+
+
+
+
+
+
+
);
}