Skip to content

Commit 5aeb04e

Browse files
committed
[ContextualSaveBar] Add support for custom saveAction markup
1 parent f1c5110 commit 5aeb04e

File tree

7 files changed

+423
-31
lines changed

7 files changed

+423
-31
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris': minor
3+
---
4+
5+
Added support for setting custom markup on the `ContextualSaveBar` `saveAction` prop

polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.module.scss

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
@import '../../../../styles/common';
22

33
.ContextualSaveBar {
4-
/* stylelint-disable -- polaris: Used to apply dark theme to action buttons */
5-
--p-color-bg-surface: var(--p-color-bg-inverse);
6-
--p-color-text: var(--p-color-text-inverse);
7-
--p-color-bg-surface-hover: var(--p-color-bg-fill-inverse-hover);
8-
--p-color-bg-surface-secondary-active: var(--p-color-bg-fill-inverse-active);
9-
/* stylelint-enable */
104
display: flex;
115
height: $top-bar-height;
126
background: var(--p-color-bg-inverse);
@@ -95,8 +89,8 @@
9589
--pc-button-bg_pressed: rgba(247, 247, 247, 1);
9690
--pc-button-bg_disabled: rgba(255, 255, 255, 0.2);
9791
--pc-button-box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.48) inset,
98-
-1px 0 0 0 rgba(255, 255, 255, 0.20) inset,
99-
1px 0 0 0 rgba(255, 255, 255, 0.20) inset,
92+
-1px 0 0 0 rgba(255, 255, 255, 0.2) inset,
93+
1px 0 0 0 rgba(255, 255, 255, 0.2) inset,
10094
0 -1.5px 0 0 rgba(0, 0, 0, 0.25) inset;
10195
--pc-button-box-shadow_active: 0px 2px 1px 0px rgba(26, 26, 26, 0.2) inset,
10296
1px 0px 1px 0px rgba(26, 26, 26, 0.12) inset,

polaris-react/src/components/Frame/components/ContextualSaveBar/ContextualSaveBar.tsx

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ import {AlertTriangleIcon} from '@shopify/polaris-icons';
33

44
import {Button} from '../../../Button';
55
import {Image} from '../../../Image';
6-
// eslint-disable-next-line import/no-deprecated
7-
import {LegacyStack} from '../../../LegacyStack';
6+
import {InlineStack} from '../../../InlineStack';
87
import {Text} from '../../../Text';
98
import {Icon} from '../../../Icon';
109
import {classNames} from '../../../../utilities/css';
10+
import type {
11+
ContextualSaveBarProps,
12+
ContextualSaveBarAction,
13+
} from '../../../../utilities/frame';
1114
import {useFrame} from '../../../../utilities/frame';
12-
import type {ContextualSaveBarProps} from '../../../../utilities/frame';
1315
import {getWidth} from '../../../../utilities/get-width';
1416
import {useI18n} from '../../../../utilities/i18n';
1517
import {useToggle} from '../../../../utilities/use-toggle';
18+
import {isInterface} from '../../../../utilities/is-interface';
1619

1720
import {DiscardConfirmationModal} from './components';
1821
import styles from './ContextualSaveBar.module.scss';
@@ -77,25 +80,34 @@ export function ContextualSaveBar({
7780
</Button>
7881
);
7982

83+
let saveActionMarkup;
84+
8085
const saveActionContent =
81-
saveAction && saveAction.content
86+
saveAction && 'content' in saveAction
8287
? saveAction.content
8388
: i18n.translate('Polaris.ContextualSaveBar.save');
8489

85-
const saveActionMarkup = saveAction && (
86-
<Button
87-
variant="primary"
88-
tone="success"
89-
size="large"
90-
url={saveAction.url}
91-
onClick={saveAction.onAction}
92-
loading={saveAction.loading}
93-
disabled={saveAction.disabled}
94-
accessibilityLabel={saveAction.content}
95-
>
96-
{saveActionContent}
97-
</Button>
98-
);
90+
if (saveAction && isInterface(saveAction)) {
91+
const {url, loading, disabled, onAction} =
92+
saveAction as ContextualSaveBarAction;
93+
94+
saveActionMarkup = (
95+
<Button
96+
variant="primary"
97+
tone="success"
98+
size="large"
99+
onClick={onAction}
100+
url={url}
101+
loading={loading}
102+
disabled={disabled}
103+
accessibilityLabel={saveActionContent}
104+
>
105+
{saveActionContent}
106+
</Button>
107+
);
108+
} else {
109+
saveActionMarkup = saveAction;
110+
}
99111

100112
const width = getWidth(logo, 104);
101113

@@ -134,11 +146,11 @@ export function ContextualSaveBar({
134146
)}
135147
</div>
136148
<div className={styles.ActionContainer}>
137-
<LegacyStack spacing="tight" wrap={false}>
149+
<InlineStack gap="200" wrap={false}>
138150
{secondaryMenu}
139151
{discardActionMarkup}
140152
{saveActionMarkup}
141-
</LegacyStack>
153+
</InlineStack>
142154
</div>
143155
</div>
144156
</div>

polaris-react/src/components/Page/Page.stories.tsx

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, {useState, useRef} from 'react';
22
import type {ComponentMeta} from '@storybook/react';
33
import {
44
DeleteIcon,
@@ -7,6 +7,7 @@ import {
77
ExternalIcon,
88
ViewIcon,
99
MenuVerticalIcon,
10+
ChevronDownIcon,
1011
} from '@shopify/polaris-icons';
1112
import {
1213
Badge,
@@ -15,6 +16,13 @@ import {
1516
LegacyStack,
1617
Page,
1718
PageActions,
19+
Popover,
20+
ActionList,
21+
ButtonGroup,
22+
TextField,
23+
FormLayout,
24+
ContextualSaveBar,
25+
Frame,
1826
} from '@shopify/polaris';
1927

2028
export default {
@@ -493,3 +501,177 @@ export function WithContentAfterTitleAndSubtitle() {
493501
</Page>
494502
);
495503
}
504+
505+
export function WithSplitSaveAction() {
506+
const initialState = {
507+
title: 'Jar With Lock-Lid',
508+
description: '',
509+
isDraft: false,
510+
};
511+
512+
const [active, setActive] = React.useState(false);
513+
const [title, setTitle] = useState('Jar With Lock-Lid');
514+
const [description, setDescription] = useState('');
515+
const [isDirty, setIsDirty] = useState(false);
516+
const [isDraft, setIsDraft] = useState(false);
517+
518+
const savedEditHistory = useRef<
519+
{
520+
title: string;
521+
description: string;
522+
isDraft: boolean;
523+
}[]
524+
>([]);
525+
526+
const handleChange = (name: string) => (value: string) => {
527+
switch (name) {
528+
case 'title':
529+
handleDirtyState(name, value, title);
530+
setTitle(value);
531+
break;
532+
case 'description':
533+
handleDirtyState(name, value, description);
534+
setDescription(value);
535+
break;
536+
default:
537+
null;
538+
}
539+
};
540+
541+
const handleDirtyState = (
542+
name: string,
543+
newValue: string,
544+
currentValue: string,
545+
) => {
546+
if (
547+
(newValue !== initialState[name] && !isDirty) ||
548+
newValue !== currentValue
549+
) {
550+
setIsDirty(true);
551+
} else {
552+
setIsDirty(false);
553+
}
554+
};
555+
556+
const handleDiscard = () => {
557+
const previousState: {
558+
title: string;
559+
description: string;
560+
isDraft: boolean;
561+
} = savedEditHistory.current.pop() ?? initialState;
562+
563+
setTitle(previousState.title);
564+
setDescription(previousState.description);
565+
setIsDraft(previousState.isDraft);
566+
setIsDirty(false);
567+
};
568+
569+
const splitButton = (
570+
<ButtonGroup variant="segmented">
571+
<Button
572+
size="large"
573+
onClick={() => {
574+
savedEditHistory.current.push({title, description, isDraft: false});
575+
setIsDirty(false);
576+
setIsDraft(false);
577+
}}
578+
>
579+
Save
580+
</Button>
581+
582+
<Popover
583+
active={active}
584+
preferredAlignment="right"
585+
activator={
586+
<Button
587+
size="large"
588+
onClick={() => setActive(true)}
589+
icon={ChevronDownIcon}
590+
accessibilityLabel="Other save actions"
591+
/>
592+
}
593+
autofocusTarget="first-node"
594+
onClose={() => setActive(false)}
595+
zIndexOverride={514}
596+
>
597+
<ActionList
598+
actionRole="menuitem"
599+
items={[
600+
{
601+
content: 'Save as draft',
602+
onAction: () => {
603+
setIsDraft(true);
604+
savedEditHistory.current.push({
605+
title,
606+
description,
607+
isDraft: true,
608+
});
609+
},
610+
},
611+
]}
612+
onActionAnyItem={() => setIsDirty(false)}
613+
/>
614+
</Popover>
615+
</ButtonGroup>
616+
);
617+
618+
const saveBar = isDirty ? (
619+
<ContextualSaveBar
620+
message="Unsaved changes"
621+
saveAction={splitButton}
622+
discardAction={{
623+
content: 'Discard',
624+
onAction: handleDiscard,
625+
}}
626+
/>
627+
) : null;
628+
629+
console.log(savedEditHistory);
630+
631+
return (
632+
<Frame
633+
logo={{
634+
width: 124,
635+
contextualSaveBarSource:
636+
'https://cdn.shopify.com/s/files/1/0446/6937/files/jaded-pixel-logo-gray.svg?6215648040070010999',
637+
}}
638+
>
639+
{saveBar}
640+
<Page
641+
backAction={{content: 'Products', url: '#'}}
642+
title="Jar With Lock-Lid"
643+
titleMetadata={
644+
<Badge tone={isDraft ? 'info' : 'success'}>
645+
{isDraft ? 'Draft' : 'Active'}
646+
</Badge>
647+
}
648+
secondaryActions={[
649+
{content: 'Duplicate'},
650+
{content: 'View on your store'},
651+
]}
652+
pagination={{
653+
hasPrevious: true,
654+
hasNext: true,
655+
}}
656+
>
657+
<LegacyCard sectioned>
658+
<FormLayout>
659+
<TextField
660+
autoComplete="off"
661+
label="Title"
662+
value={title}
663+
onChange={handleChange('title')}
664+
/>
665+
<TextField
666+
multiline
667+
autoComplete="off"
668+
label="Description"
669+
value={description}
670+
onChange={handleChange('description')}
671+
/>
672+
</FormLayout>
673+
</LegacyCard>
674+
</Page>
675+
</Frame>
676+
);
677+
}

polaris-react/src/utilities/frame/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './hooks';
33
export * from './context';
44

55
export type {
6+
ContextualSaveBarAction,
67
ContextualSaveBarProps,
78
ToastProps,
89
ToastID,

polaris-react/src/utilities/frame/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface Logo {
1313
width?: number;
1414
}
1515

16-
interface ContextualSaveBarAction {
16+
export interface ContextualSaveBarAction {
1717
/** A destination to link to */
1818
url?: string;
1919
/** Content the action displays */
@@ -40,7 +40,7 @@ export interface ContextualSaveBarProps {
4040
/** Accepts a string of content that will be rendered to the left of the actions */
4141
message?: string;
4242
/** Save or commit contextual save bar action with text defaulting to 'Save' */
43-
saveAction?: ContextualSaveBarAction;
43+
saveAction?: ContextualSaveBarAction | React.JSX.Element;
4444
/** Discard or cancel contextual save bar action with text defaulting to 'Discard' */
4545
discardAction?: ContextualSaveBarCombinedActionProps;
4646
/** Remove the normal max-width on the contextual save bar */

0 commit comments

Comments
 (0)