diff --git a/.changeset/nine-worms-sip.md b/.changeset/nine-worms-sip.md new file mode 100644 index 00000000000..7ebd6eab697 --- /dev/null +++ b/.changeset/nine-worms-sip.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': patch +--- + +Rebuilt `ResourceItem` with layout components diff --git a/polaris-react/src/components/BulkActions/BulkActions.scss b/polaris-react/src/components/BulkActions/BulkActions.scss index c0ee4ef6f1e..637288c0b90 100644 --- a/polaris-react/src/components/BulkActions/BulkActions.scss +++ b/polaris-react/src/components/BulkActions/BulkActions.scss @@ -75,6 +75,7 @@ $bulk-actions-button-stacking-order: ( width: auto; justify-content: flex-start; margin-right: var(--p-space-2); + margin-left: calc(-1 * var(--p-space-05)); } .Group-measuring & { @@ -89,6 +90,7 @@ $bulk-actions-button-stacking-order: ( .CheckableContainer { flex: 1 1 0; + margin-left: calc(-1 * (var(--p-space-05) + var(--p-space-1))); } .disabled { diff --git a/polaris-react/src/components/ResourceItem/ResourceItem.scss b/polaris-react/src/components/ResourceItem/ResourceItem.scss index 66b84fe3fe5..151bc5b7e56 100644 --- a/polaris-react/src/components/ResourceItem/ResourceItem.scss +++ b/polaris-react/src/components/ResourceItem/ResourceItem.scss @@ -1,50 +1,31 @@ @import '../../styles/common'; @mixin action-hide { - clip: rect(1px, 1px, 1px, 1px); + clip: rect(0, 0, 0, 0); overflow: hidden; - height: 1px; } @mixin action-unhide { clip: auto; overflow: visible; - height: 100%; -} - -.CheckboxWrapper { - display: flex; } .ResourceItem { --pc-resource-item-min-height: 44px; --pc-resource-item-disclosure-width: 48px; - // Offset equals handle width + handle margin-left + handle margin-right - --pc-resource-item-offset: 40px; + --pc-resource-item-offset: 38px; --pc-resource-item-clickable-stacking-order: 1; --pc-resource-item-content-stacking-order: 2; - position: relative; outline: none; cursor: pointer; - &:not(.persistActions) { - .Actions { - right: var(--p-space-4); - } - } - &:hover { background-color: var(--p-surface-hovered); - &:not(.persistActions) { - // stylelint-disable-next-line selector-max-specificity - .Actions { + .Actions { + /* stylelint-disable-next-line selector-max-combinators */ + > * { @include action-unhide; - - // stylelint-disable-next-line max-nesting-depth - @media #{$p-breakpoints-lg-down} { - display: none; - } } } } @@ -82,67 +63,6 @@ border: none; } -// Item inner container -.Container { - position: relative; - z-index: var(--pc-resource-item-content-stacking-order); - padding: var(--p-space-3) var(--p-space-4); - min-height: var(--pc-resource-item-min-height); - display: flex; - align-items: flex-start; - - @media #{$p-breakpoints-sm-up} { - padding: var(--p-space-3) var(--p-space-5); - } -} - -.alignmentLeading { - align-items: flex-start; -} - -.alignmentTrailing { - align-items: flex-end; -} - -.alignmentCenter { - align-items: center; -} - -.alignmentFill { - align-items: stretch; -} - -.alignmentBaseline { - align-items: baseline; -} - -.Owned { - display: flex; -} - -.OwnedNoMedia { - padding-top: var(--p-space-1); -} - -// Item handle -.Handle { - width: 48px; - min-height: var(--pc-resource-item-min-height); - justify-content: center; - align-items: center; - margin: calc(-1 * var(--p-space-3)) var(--p-space-1) - calc(-1 * var(--p-space-3)) calc(-1 * var(--p-space-3)); - display: flex; - - @media #{$p-breakpoints-sm-down} { - visibility: hidden; - - .selectMode & { - visibility: visible; - } - } -} - .selectable { width: calc(100% + var(--pc-resource-item-offset)); transform: translateX(calc(-1 * var(--pc-resource-item-offset))); @@ -160,78 +80,20 @@ } } -.Media { - flex: 0 0 auto; - margin-right: var(--p-space-5); - color: inherit; - text-decoration: none; -} - -// Item content -.Content { - @include layout-flex-fix; - flex: 1 1 auto; -} - // Item actions .Actions { - position: absolute; - top: 0; - display: flex; - pointer-events: initial; - height: 100%; - max-height: 56px; - - @include action-hide; - - .focused & { - @include action-unhide; + > * { + @include action-hide; } - @media #{$p-breakpoints-lg-down} { - display: none; - } -} - -.persistActions { - .Actions { - position: relative; - display: flex; - flex: 0 0 auto; - flex-basis: auto; - align-items: center; - margin-top: 0; - margin-left: var(--p-space-4); - pointer-events: initial; - height: 100%; - - @media #{$p-breakpoints-lg-down} { - display: none; + .focused & { + // stylelint-disable-next-line selector-max-combinators + > * { + @include action-unhide; } } } -.Disclosure { - position: relative; - top: calc(-1 * var(--p-space-3)); - right: calc(-1 * var(--p-space-4)); - display: none; - width: var(--pc-resource-item-disclosure-width); - min-height: var(--pc-resource-item-min-height); - pointer-events: initial; - - .selectMode & { - display: none; - } - - @media #{$p-breakpoints-lg-down} { - display: flex; - flex: 0 0 var(--pc-resource-item-disclosure-width); - justify-content: center; - align-items: center; - } -} - .selected { background-color: var(--p-surface-selected); @@ -245,7 +107,6 @@ } .ListItem { - position: relative; @include focus-ring($border-width: -1px); .ListItem + & { diff --git a/polaris-react/src/components/ResourceItem/ResourceItem.tsx b/polaris-react/src/components/ResourceItem/ResourceItem.tsx index c06f4da4432..8c319283ae9 100644 --- a/polaris-react/src/components/ResourceItem/ResourceItem.tsx +++ b/polaris-react/src/components/ResourceItem/ResourceItem.tsx @@ -2,23 +2,31 @@ import React, {Component, createRef, useContext} from 'react'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; import isEqual from 'react-fast-compare'; -import {classNames, variationName} from '../../utilities/css'; -import {useI18n} from '../../utilities/i18n'; -import type {DisableableAction} from '../../types'; import {ActionList} from '../ActionList'; +import {Box} from '../Box'; +import {Bleed} from '../Bleed'; +import {Button, buttonsFrom} from '../Button'; +import {ButtonGroup} from '../ButtonGroup'; +import {Checkbox} from '../Checkbox'; +import {Columns} from '../Columns'; +import {Inline, InlineProps} from '../Inline'; import {Popover} from '../Popover'; -import type {AvatarProps} from '../Avatar'; import {UnstyledLink} from '../UnstyledLink'; +import type {AvatarProps} from '../Avatar'; +import type {DisableableAction} from '../../types'; import type {ThumbnailProps} from '../Thumbnail'; -import {ButtonGroup} from '../ButtonGroup'; -import {Checkbox} from '../Checkbox'; -import {Button, buttonsFrom} from '../Button'; +import { + useBreakpoints, + BreakpointsDirectionAlias, +} from '../../utilities/breakpoints'; +import {classNames} from '../../utilities/css'; +import {globalIdGeneratorFactory} from '../../utilities/unique-id'; +import {useI18n} from '../../utilities/i18n'; import { ResourceListContext, SELECT_ALL_ITEMS, ResourceListSelectedItems, } from '../../utilities/resource-list'; -import {globalIdGeneratorFactory} from '../../utilities/unique-id'; import styles from './ResourceItem.scss'; @@ -68,8 +76,11 @@ interface PropsWithClick extends BaseProps { } export type ResourceItemProps = PropsWithUrl | PropsWithClick; - +type BreakpointsMatches = { + [DirectionAlias in BreakpointsDirectionAlias]: boolean; +}; interface PropsFromWrapper { + breakpoints?: BreakpointsMatches; context: React.ContextType; i18n: ReturnType; } @@ -151,6 +162,7 @@ class BaseResourceItem extends Component { i18n, verticalAlignment, dataHref, + breakpoints, } = this.props; const {actionsMenuVisible, focused, focusedInner, selected} = this.state; @@ -158,42 +170,47 @@ class BaseResourceItem extends Component { let ownedMarkup: React.ReactNode = null; let handleMarkup: React.ReactNode = null; - const mediaMarkup = media ? ( -
{media}
- ) : null; - if (selectable) { const checkboxAccessibilityLabel = name || accessibilityLabel || i18n.translate('Polaris.Common.checkbox'); handleMarkup = ( -
-
-
- -
-
+
+ + +
+
+ +
+
+
+
); } if (media || selectable) { ownedMarkup = ( -
{handleMarkup} - {mediaMarkup} -
+ {media} + ); } @@ -217,15 +234,13 @@ class BaseResourceItem extends Component { if (shortcutActions && !loading) { if (persistActions) { - actionsMarkup = ( + actionsMarkup = breakpoints?.lgUp ? (
- {buttonsFrom(shortcutActions, { - plain: true, - })} + {buttonsFrom(shortcutActions, {plain: true})}
- ); + ) : null; const disclosureAccessibilityLabel = name ? i18n.translate('Polaris.ResourceList.Item.actionsDropdownLabel', { @@ -233,54 +248,68 @@ class BaseResourceItem extends Component { }) : i18n.translate('Polaris.ResourceList.Item.actionsDropdown'); - disclosureMarkup = ( -
- - } - onClose={this.handleCloseRequest} - active={actionsMenuVisible} - > - - -
- ); - } else { + disclosureMarkup = + !selectMode && breakpoints?.lgDown ? ( +
+ + } + onClose={this.handleCloseRequest} + active={actionsMenuVisible} + > + + +
+ ) : null; + } else if (breakpoints?.lgUp) { actionsMarkup = (
- - {buttonsFrom(shortcutActions, { - size: 'slim', - })} - + + + {buttonsFrom(shortcutActions, {size: 'slim'})} + +
); } } - const content = children ? ( -
{children}
- ) : null; - - const containerClassName = classNames( - styles.Container, - verticalAlignment && - styles[variationName('alignment', verticalAlignment)], - ); - const containerMarkup = ( -
- {ownedMarkup} - {content} - {actionsMarkup} - {disclosureMarkup} -
+ + + + {ownedMarkup} + + + {children} + + + + {actionsMarkup} + {disclosureMarkup} + + ); const tabIndex = loading ? -1 : 0; @@ -459,11 +488,30 @@ function isSelected(id: string, selectedItems?: ResourceListSelectedItems) { } export function ResourceItem(props: ResourceItemProps) { + const breakpoints = useBreakpoints(); return ( ); } + +function getAlignment(alignment?: Alignment): InlineProps['blockAlign'] { + switch (alignment) { + case 'leading': + return 'start'; + case 'trailing': + return 'end'; + case 'center': + return 'center'; + case 'fill': + return 'stretch'; + case 'baseline': + return 'baseline'; + default: + return 'start'; + } +} diff --git a/polaris-react/src/components/ResourceItem/tests/ResourceItem.test.tsx b/polaris-react/src/components/ResourceItem/tests/ResourceItem.test.tsx index d8c9b66d699..8b584bea38c 100644 --- a/polaris-react/src/components/ResourceItem/tests/ResourceItem.test.tsx +++ b/polaris-react/src/components/ResourceItem/tests/ResourceItem.test.tsx @@ -1,10 +1,13 @@ import React, {AllHTMLAttributes} from 'react'; import {mountWithApp} from 'tests/utilities'; +import {matchMedia} from '@shopify/jest-dom-mocks'; +import {setMediaWidth} from 'tests/utilities/breakpoints'; import {Avatar} from '../../Avatar'; import {Button} from '../../Button'; import {ButtonGroup} from '../../ButtonGroup'; import {Checkbox} from '../../Checkbox'; +import {Inline} from '../../Inline'; import {Thumbnail} from '../../Thumbnail'; import {UnstyledLink} from '../../UnstyledLink'; import {ResourceItem} from '../ResourceItem'; @@ -17,10 +20,12 @@ describe('', () => { beforeEach(() => { spy = jest.spyOn(window, 'open'); spy.mockImplementation(() => {}); + matchMedia.mock(); }); afterEach(() => { spy.mockRestore(); + matchMedia.restore(); }); const mockDefaultContext = { @@ -126,6 +131,7 @@ describe('', () => { }); it('is used on the disclosure action menu when there are persistent actions', () => { + setMediaWidth('breakpoints-lg'); const item = mountWithApp( ', () => { , ); - wrapper.find('div', {className: styles.Handle})!.trigger('onClick', { + wrapper.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {}, }); @@ -420,7 +426,7 @@ describe('', () => { , ); - wrapper.find('div', {className: styles.Handle})!.trigger('onClick', { + wrapper.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {shiftKey: false}, }); @@ -545,8 +551,7 @@ describe('', () => { } /> , ); - const media = wrapper.find('div', {className: styles.Media}); - expect(media).toContainReactComponent(Avatar); + expect(wrapper).toContainReactComponent(Avatar); }); it('includes a if one is provided', () => { @@ -559,8 +564,7 @@ describe('', () => { /> , ); - const media = wrapper.find('div', {className: styles.Media}); - expect(media).toContainReactComponent(Thumbnail); + expect(wrapper).toContainReactComponent(Thumbnail); }); }); @@ -576,7 +580,8 @@ describe('', () => { }); }); - it('renders shortcut actions when some are provided', () => { + it('renders shortcut actions when some are provided and viewport is lgUp', () => { + setMediaWidth('breakpoints-lg'); const wrapper = mountWithApp( ', () => { }); }); - it('renders persistent shortcut actions if persistActions is true', () => { + it('renders persistent shortcut actions if persistActions is true and viewport is lgUp', () => { + setMediaWidth('breakpoints-lg'); const wrapper = mountWithApp( ', () => { it('renders with default flex-start alignment if not provided', () => { const resourceItem = mountWithApp(); - expect(resourceItem).toContainReactComponent('div', { - className: 'Container', - }); + expect(resourceItem).toContainReactComponent(Inline); }); it('renders with leading vertical alignment', () => { @@ -689,8 +693,8 @@ describe('', () => { , ); - expect(resourceItem).toContainReactComponent('div', { - className: 'Container alignmentLeading', + expect(resourceItem).toContainReactComponent(Inline, { + blockAlign: 'start', }); }); @@ -699,8 +703,8 @@ describe('', () => { , ); - expect(resourceItem).toContainReactComponent('div', { - className: 'Container alignmentCenter', + expect(resourceItem).toContainReactComponent(Inline, { + blockAlign: 'center', }); }); @@ -709,8 +713,8 @@ describe('', () => { , ); - expect(resourceItem).toContainReactComponent('div', { - className: 'Container alignmentTrailing', + expect(resourceItem).toContainReactComponent(Inline, { + blockAlign: 'end', }); }); @@ -719,8 +723,8 @@ describe('', () => { , ); - expect(resourceItem).toContainReactComponent('div', { - className: 'Container alignmentFill', + expect(resourceItem).toContainReactComponent(Inline, { + blockAlign: 'stretch', }); }); @@ -729,8 +733,8 @@ describe('', () => { , ); - expect(resourceItem).toContainReactComponent('div', { - className: 'Container alignmentBaseline', + expect(resourceItem).toContainReactComponent(Inline, { + blockAlign: 'baseline', }); }); }); diff --git a/polaris-react/src/components/ResourceList/ResourceList.scss b/polaris-react/src/components/ResourceList/ResourceList.scss index e0d7039f080..080aa359d82 100644 --- a/polaris-react/src/components/ResourceList/ResourceList.scss +++ b/polaris-react/src/components/ResourceList/ResourceList.scss @@ -167,6 +167,7 @@ $breakpoints-empty-search-results-height-up: '(min-height: #{breakpoint(600px)}) .CheckableButtonWrapper { display: none; + margin-left: calc(-1 * var(--p-space-05)); @media #{$p-breakpoints-sm-up} { flex: 1; diff --git a/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx b/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx index 15bf63a6e0f..71b323c5604 100644 --- a/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx +++ b/polaris-react/src/components/ResourceList/tests/ResourceList.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {mountWithApp} from 'tests/utilities'; +import {matchMedia} from '@shopify/jest-dom-mocks'; import {BulkActions} from '../../BulkActions'; import {Button} from '../../Button'; @@ -45,6 +46,14 @@ const alternateTool =
Alternate Tool
; const defaultWindowWidth = window.innerWidth; describe('', () => { + beforeEach(() => { + matchMedia.mock(); + }); + + afterEach(() => { + matchMedia.restore(); + }); + describe('renderItem', () => { it('renders list items', () => { const resourceList = mountWithApp( @@ -340,7 +349,9 @@ describe('', () => { onSelectionChange={onSelectionChange} />, ); - resourceList.find('div', {className: styles.Handle})!.trigger('onClick', { + const resourceItem = resourceList.find(ResourceItem); + + resourceItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {}, }); @@ -1059,13 +1070,13 @@ describe('', () => { />, ); const firstItem = resourceList.find(ResourceItem); - firstItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + firstItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {}, }); const allItems = resourceList.findAll(ResourceItem); const lastItem = allItems[allItems.length - 1]; - lastItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + lastItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {shiftKey: true}, }); @@ -1084,13 +1095,13 @@ describe('', () => { />, ); const firstItem = resourceList.find(ResourceItem); - firstItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + firstItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {}, }); const allItems = resourceList.findAll(ResourceItem); const lastItem = allItems[allItems.length - 1]; - lastItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + lastItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {shiftKey: true}, }); @@ -1128,13 +1139,13 @@ describe('', () => { />, ); const firstItem = resourceList.find(ResourceItem); - firstItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + firstItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {}, }); const allItems = resourceList.findAll(ResourceItem); const lastItem = allItems[allItems.length - 1]; - lastItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + lastItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {shiftKey: true}, }); @@ -1160,13 +1171,13 @@ describe('', () => { ); // Sets {lastSelected: 0} const firstItem = resourceList.find(ResourceItem); - firstItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + firstItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {}, }); const allItems = resourceList.findAll(ResourceItem); const lastItem = allItems[allItems.length - 1]; - lastItem!.find('div', {className: styles.Handle})!.trigger('onClick', { + lastItem!.findAll('div')[6]!.trigger('onClick', { stopPropagation: () => {}, nativeEvent: {shiftKey: true}, });