From efb1bfdcbe4b6fd94b19921f64d7b043b1d4d36f Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Thu, 11 Jan 2024 18:02:25 +0000 Subject: [PATCH 1/6] chore: use tabs method to resize actions --- .../components/Actions/Actions.module.scss | 17 + .../ActionMenu/components/Actions/Actions.tsx | 331 ++++++------------ .../ActionsMeasurer/ActionsMeasurer.tsx | 113 ++++++ .../components/ActionsMeasurer/index.tsx | 2 + .../components/Actions/components/index.ts | 2 + .../components/Actions/tests/Actions.test.tsx | 107 ++++++ .../Actions/tests/utilities.test.ts | 121 +++++++ .../components/Actions/utilities.ts | 65 ++++ .../SecondaryAction/SecondaryAction.tsx | 13 +- .../src/components/Page/Page.stories.tsx | 115 +++++- 10 files changed, 647 insertions(+), 239 deletions(-) create mode 100644 polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx create mode 100644 polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/index.tsx create mode 100644 polaris-react/src/components/ActionMenu/components/Actions/components/index.ts create mode 100644 polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts create mode 100644 polaris-react/src/components/ActionMenu/components/Actions/utilities.ts diff --git a/polaris-react/src/components/ActionMenu/components/Actions/Actions.module.scss b/polaris-react/src/components/ActionMenu/components/Actions/Actions.module.scss index 75d3d26e71b..69199b08925 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/Actions.module.scss +++ b/polaris-react/src/components/ActionMenu/components/Actions/Actions.module.scss @@ -6,6 +6,23 @@ align-items: center; justify-content: flex-end; flex: 1 1 auto; + gap: var(--p-space-200); + + > * { + flex: 0 0 auto; + } +} + +.ActionsLayoutMeasurer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + flex: 1 1 auto; + gap: 0; + padding: 0; + visibility: hidden; + height: 0; > * { flex: 0 0 auto; diff --git a/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx b/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx index 8620d15a288..3fe9fe55398 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx +++ b/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; import type { ActionListItemDescriptor, @@ -7,14 +7,13 @@ import type { MenuGroupDescriptor, } from '../../../../types'; import {MenuGroup} from '../MenuGroup'; -import {ButtonGroup} from '../../../ButtonGroup'; -import {debounce} from '../../../../utilities/debounce'; import {useI18n} from '../../../../utilities/i18n'; import {SecondaryAction} from '../SecondaryAction'; -import {useEventListener} from '../../../../utilities/use-event-listener'; -import {useIsomorphicLayoutEffect} from '../../../../utilities/use-isomorphic-layout-effect'; import styles from './Actions.module.scss'; +import type {ActionsMeasurements} from './components'; +import {ActionsMeasurer} from './components'; +import {getVisibleAndHiddenActionsIndices} from './utilities'; interface Props { /** Collection of page-level secondary actions */ @@ -24,40 +23,33 @@ interface Props { /** Callback that returns true when secondary actions are rolled up into action groups, and false when not */ onActionRollup?(hasRolledUp: boolean): void; } - -interface MeasuredActions { - showable: MenuActionDescriptor[]; - rolledUp: (MenuActionDescriptor | MenuGroupDescriptor)[]; +interface MeasuredActionsIndices { + visibleActions: number[]; + hiddenActions: number[]; + visibleGroups: number[]; + hiddenGroups: number[]; } -const ACTION_SPACING = 8; - export function Actions({actions = [], groups = [], onActionRollup}: Props) { const i18n = useI18n(); - const actionsLayoutRef = useRef(null); - const menuGroupWidthRef = useRef(0); - const availableWidthRef = useRef(0); - const actionsAndGroupsLengthRef = useRef(0); - const timesMeasured = useRef(0); - const actionWidthsRef = useRef([]); const rollupActiveRef = useRef(null); const [activeMenuGroup, setActiveMenuGroup] = useState( undefined, ); - const [measuredActions, setMeasuredActions] = useState({ - showable: [], - rolledUp: [], + const [ + {visibleActions, hiddenActions, visibleGroups, hiddenGroups}, + setMeasuredActionsIndices, + ] = useState({ + visibleActions: [], + hiddenActions: [], + visibleGroups: [], + hiddenGroups: [], }); + const defaultRollupGroup: MenuGroupDescriptor = { title: i18n.translate('Polaris.ActionMenu.Actions.moreActions'), actions: [], }; - const lastMenuGroup = [...groups].pop(); - const lastMenuGroupWidth = [...actionWidthsRef.current].pop() || 0; - - const handleActionsOffsetWidth = useCallback((width: number) => { - actionWidthsRef.current = [...actionWidthsRef.current, width]; - }, []); const handleMenuGroupToggle = useCallback( (group: string) => setActiveMenuGroup(activeMenuGroup ? undefined : group), @@ -69,179 +61,50 @@ export function Actions({actions = [], groups = [], onActionRollup}: Props) { [], ); - const updateActions = useCallback(() => { - let actionsAndGroups = [...actions, ...groups]; - - if (groups.length > 0) { - // We don't want to include actions from the last group - // since it is always rendered with its own actions - actionsAndGroups = [...actionsAndGroups].slice( - 0, - actionsAndGroups.length - 1, - ); - } - - setMeasuredActions((currentMeasuredActions) => { - const showable = actionsAndGroups.slice( - 0, - currentMeasuredActions.showable.length, - ); - const rolledUp = actionsAndGroups.slice( - currentMeasuredActions.showable.length, - actionsAndGroups.length, - ); - - return {showable, rolledUp}; - }); - }, [actions, groups]); - - const measureActions = useCallback(() => { - if ( - actionWidthsRef.current.length === 0 || - availableWidthRef.current === 0 - ) { - return; - } - - const actionsAndGroups = [...actions, ...groups]; - - if (actionsAndGroups.length === 1) { - setMeasuredActions({showable: actionsAndGroups, rolledUp: []}); - return; - } - - let currentAvailableWidth = availableWidthRef.current; - let newShowableActions: MenuActionDescriptor[] = []; - let newRolledUpActions: (MenuActionDescriptor | MenuGroupDescriptor)[] = []; - - actionsAndGroups.forEach((action, index) => { - const canFitAction = - actionWidthsRef.current[index] + - menuGroupWidthRef.current + - ACTION_SPACING + - lastMenuGroupWidth <= - currentAvailableWidth; - - if (canFitAction) { - currentAvailableWidth -= - actionWidthsRef.current[index] + ACTION_SPACING * 2; - newShowableActions = [...newShowableActions, action]; - } else { - currentAvailableWidth = 0; - // Find last group if it exists and always render it as a rolled up action below - if (action === lastMenuGroup) return; - newRolledUpActions = [...newRolledUpActions, action]; - } - }); - - if (onActionRollup) { - // Note: Do not include last group actions since we are skipping `lastMenuGroup` above - // as it is always rendered with its own actions - const isRollupActive = - newShowableActions.length < actionsAndGroups.length - 1; - if (rollupActiveRef.current !== isRollupActive) { - onActionRollup(isRollupActive); - rollupActiveRef.current = isRollupActive; - } - } - - setMeasuredActions({ - showable: newShowableActions, - rolledUp: newRolledUpActions, - }); - - timesMeasured.current += 1; - actionsAndGroupsLengthRef.current = actionsAndGroups.length; - }, [actions, groups, lastMenuGroup, lastMenuGroupWidth, onActionRollup]); - - const handleResize = useMemo( - () => - debounce( - () => { - if (!actionsLayoutRef.current) return; - availableWidthRef.current = actionsLayoutRef.current.offsetWidth; - // Set timesMeasured to 0 to allow re-measuring - timesMeasured.current = 0; - measureActions(); - }, - 50, - {leading: false, trailing: true}, - ), - [measureActions], - ); - - useEventListener('resize', handleResize); - - useIsomorphicLayoutEffect(() => { - if (!actionsLayoutRef.current) return; - - availableWidthRef.current = actionsLayoutRef.current.offsetWidth; - - if ( - // Allow measuring twice - // This accounts for the initial paint and re-flow - timesMeasured.current >= 2 && - [...actions, ...groups].length === actionsAndGroupsLengthRef.current - ) { - updateActions(); - return; - } - measureActions(); - }, [actions, groups, measureActions, updateActions]); - - const actionsMarkup = actions.map((action) => { - if ( - measuredActions.showable.length > 0 || - measuredActions.rolledUp.includes(action) - ) + const actionsMarkup = actions.map((action, index) => { + if (!visibleActions.includes(index)) { return null; + } const {content, onAction, ...rest} = action; return ( - + {content} ); }); - const rollUppableActionsMarkup = - measuredActions.showable.length > 0 - ? measuredActions.showable.map( - (action) => - action.content && ( - - {action.content} - - ), - ) - : null; + const groupsToFilters = + hiddenGroups.length > 0 || hiddenActions.length > 0 + ? [...groups, defaultRollupGroup] + : [...groups]; + + const filteredGroups = groupsToFilters.filter((group, index) => { + const hasNoGroupsProp = groups.length === 0; + const isVisibleGroup = visibleGroups.includes(index); + const isDefaultGroup = group === defaultRollupGroup; + + if (hasNoGroupsProp) { + return hiddenActions.length > 0; + } - const filteredGroups = [...groups, defaultRollupGroup].filter((group) => { - return groups.length === 0 - ? group - : group === lastMenuGroup || - !measuredActions.rolledUp.some( - (rolledUpGroup) => - isMenuGroup(rolledUpGroup) && rolledUpGroup.title === group.title, - ); + if (isDefaultGroup) { + return true; + } + + return isVisibleGroup; }); + const hiddenActionObjects = hiddenActions.map((index) => actions[index]); + const hiddenGroupObjects = hiddenGroups.map((index) => groups[index]); + const groupsMarkup = filteredGroups.map((group) => { const {title, actions: groupActions, ...rest} = group; const isDefaultGroup = group === defaultRollupGroup; - const isLastMenuGroup = group === lastMenuGroup; + const allHiddenItems = [...hiddenActionObjects, ...hiddenGroupObjects]; const [finalRolledUpActions, finalRolledUpSectionGroups] = - measuredActions.rolledUp.reduce( + allHiddenItems.reduce( ([actions, sections], action) => { if (isMenuGroup(action)) { sections.push({ @@ -259,7 +122,7 @@ export function Actions({actions = [], groups = [], onActionRollup}: Props) { }, [[] as ActionListItemDescriptor[], [] as ActionListSection[]], ); - if (!isDefaultGroup && !isLastMenuGroup) { + if (!isDefaultGroup) { // Render a normal MenuGroup with just its actions return ( - ); - } else if (!isDefaultGroup && isLastMenuGroup) { - // render the last, rollup group with its actions and finalRolledUpActions - return ( - - ); - } else if ( - isDefaultGroup && - groups.length === 0 && - finalRolledUpActions.length - ) { - // Render the default group to rollup into if one does not exist - return ( - ); } + return ( + + ); }); - const groupedActionsMarkup = ( - - {rollUppableActionsMarkup} - {actionsMarkup} - {groupsMarkup} - + const handleMeasurement = useCallback( + (measurements: ActionsMeasurements) => { + const { + hiddenActionsWidths: actionsWidths, + containerWidth, + disclosureWidth, + } = measurements; + + const {visibleActions, hiddenActions, visibleGroups, hiddenGroups} = + getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + actionsWidths, + containerWidth, + ); + + if (onActionRollup) { + const isRollupActive = + hiddenActions.length > 0 || hiddenGroups.length > 0; + if (rollupActiveRef.current !== isRollupActive) { + onActionRollup(isRollupActive); + rollupActiveRef.current = isRollupActive; + } + } + setMeasuredActionsIndices({ + visibleActions, + hiddenActions, + visibleGroups, + hiddenGroups, + }); + }, + [actions, groups, onActionRollup], + ); + + const actionsMeasurer = ( + ); return ( -
- {groupedActionsMarkup} +
+ {actionsMeasurer} +
+ {actionsMarkup} + {groupsMarkup} +
); } diff --git a/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx new file mode 100644 index 00000000000..800ba40fa4a --- /dev/null +++ b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx @@ -0,0 +1,113 @@ +import React, {useCallback, useRef, useEffect} from 'react'; + +import type { + MenuActionDescriptor, + MenuGroupDescriptor, +} from '../../../../../../types'; +import {useComponentDidMount} from '../../../../../../utilities/use-component-did-mount'; +import {useI18n} from '../../../../../../utilities/i18n'; +import {SecondaryAction} from '../../../SecondaryAction'; +import {useEventListener} from '../../../../../../utilities/use-event-listener'; +import styles from '../../Actions.module.scss'; + +export interface ActionsMeasurements { + containerWidth: number; + disclosureWidth: number; + hiddenActionsWidths: number[]; +} + +export interface ActionsMeasurerProps { + /** Collection of page-level secondary actions */ + actions?: MenuActionDescriptor[]; + /** Collection of page-level action groups */ + groups?: MenuGroupDescriptor[]; + handleMeasurement(measurements: ActionsMeasurements): void; +} + +const ACTION_SPACING = 8; + +export function ActionsMeasurer({ + actions = [], + groups = [], + handleMeasurement: handleMeasurementProp, +}: ActionsMeasurerProps) { + const i18n = useI18n(); + const containerNode = useRef(null); + const animationFrame = useRef(null); + + const defaultRollupGroup: MenuGroupDescriptor = { + title: i18n.translate('Polaris.ActionMenu.Actions.moreActions'), + actions: [], + }; + + const activator = ( + {defaultRollupGroup.title} + ); + + const handleMeasurement = useCallback(() => { + if (animationFrame.current) { + cancelAnimationFrame(animationFrame.current); + } + + animationFrame.current = requestAnimationFrame(() => { + if (!containerNode.current) { + return; + } + + const containerWidth = containerNode.current.offsetWidth - 20 - 28; + const hiddenActionNodes = containerNode.current.children; + const hiddenActionNodesArray = Array.from(hiddenActionNodes); + const hiddenActionsWidths = hiddenActionNodesArray.map((node) => { + const buttonWidth = Math.ceil(node.getBoundingClientRect().width); + return buttonWidth + ACTION_SPACING; + }); + const disclosureWidth = hiddenActionsWidths.pop() || 0; + + handleMeasurementProp({ + containerWidth, + disclosureWidth, + hiddenActionsWidths, + }); + }); + }, [handleMeasurementProp]); + + useEffect(() => { + console.log('should fire handle measurement'); + handleMeasurement(); + }, [handleMeasurement, actions, groups]); + + useComponentDidMount(() => { + if (process.env.NODE_ENV === 'development') { + setTimeout(handleMeasurement, 0); + } + }); + + const actionsMarkup = actions.map((action) => { + const {content, onAction, ...rest} = action; + + return ( + + {content} + + ); + }); + + const groupsMarkup = groups.map((group) => { + const {title, icon} = group; + return ( + + {title} + + ); + }); + + useEventListener('resize', handleMeasurement); + + return ( +
+ {actionsMarkup} + {groupsMarkup} + {activator} +
+ ); +} diff --git a/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/index.tsx b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/index.tsx new file mode 100644 index 00000000000..204ec1ca79e --- /dev/null +++ b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/index.tsx @@ -0,0 +1,2 @@ +export {ActionsMeasurer} from './ActionsMeasurer'; +export {type ActionsMeasurements} from './ActionsMeasurer'; diff --git a/polaris-react/src/components/ActionMenu/components/Actions/components/index.ts b/polaris-react/src/components/ActionMenu/components/Actions/components/index.ts new file mode 100644 index 00000000000..204ec1ca79e --- /dev/null +++ b/polaris-react/src/components/ActionMenu/components/Actions/components/index.ts @@ -0,0 +1,2 @@ +export {ActionsMeasurer} from './ActionsMeasurer'; +export {type ActionsMeasurements} from './ActionsMeasurer'; diff --git a/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx b/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx index 16cbcad4827..521e4904290 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx +++ b/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx @@ -5,6 +5,28 @@ import {ActionMenu} from '../../..'; import type {ActionMenuProps} from '../../..'; import {Actions, MenuGroup, RollupActions, SecondaryAction} from '../..'; import {Tooltip} from '../../../../Tooltip'; +import type {getVisibleAndHiddenActionsIndices} from '../utilities'; +import {ActionsMeasurer} from '../components'; + +jest.mock('../components/ActionsMeasurer', () => ({ + ActionsMeasurer() { + return null; + }, +})); + +jest.mock('../utilities', () => ({ + ...jest.requireActual('../utilities'), + getVisibleAndHiddenActionsIndices: jest.fn(), +})); + +function mockGetVisibleAndHiddenActionsIndices( + args: ReturnType, +) { + const getVisibleAndHiddenActionsIndices: jest.Mock = + jest.requireMock('../utilities').getVisibleAndHiddenActionsIndices; + + getVisibleAndHiddenActionsIndices.mockReturnValue(args); +} describe('', () => { const mockProps: ActionMenuProps = { @@ -29,6 +51,12 @@ describe('', () => { describe('Actions', () => { it('renders SecondaryActions', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [0, 1, 2], + visibleGroups: [], + hiddenActions: [], + hiddenGroups: [], + }); const actionsBeforeOverriddenOrder: ActionMenuProps['actions'] = [ {content: 'mock content 0'}, {content: 'mock content 1'}, @@ -39,10 +67,24 @@ describe('', () => { , ); + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); + expect(wrapper.findAll(SecondaryAction)).toHaveLength(3); }); it('renders a when helpText is set on an action', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [0], + visibleGroups: [], + hiddenActions: [], + hiddenGroups: [], + }); const toolTipAction = { content: 'Refund', helpText: @@ -50,6 +92,15 @@ describe('', () => { }; const wrapper = mountWithApp(); + + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); + const action = wrapper.find(SecondaryAction); expect(action).toContainReactComponent(Tooltip, { @@ -58,14 +109,34 @@ describe('', () => { }); it('renders a MenuGroup', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [], + visibleGroups: [0], + hiddenActions: [], + hiddenGroups: [], + }); const wrapper = mountWithApp( , ); + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); + expect(wrapper.findAll(MenuGroup)).toHaveLength(1); }); it('updates actions when they change', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [0, 1], + visibleGroups: [], + hiddenActions: [], + hiddenGroups: [], + }); function ActionsWithToggle() { const initialActions: ActionMenuProps['actions'] = [ {content: 'initial'}, @@ -87,6 +158,14 @@ describe('', () => { const wrapper = mountWithApp(); + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); + wrapper.find('button')!.trigger('onClick'); expect(wrapper).toContainReactComponent(SecondaryAction, { children: 'updated', @@ -94,6 +173,12 @@ describe('', () => { }); it('updates groups when they change', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [], + visibleGroups: [0, 1], + hiddenActions: [], + hiddenGroups: [], + }); function ActionsWithToggle() { const initialGroups: ActionMenuProps['groups'] = [ {title: 'initial', actions: [{content: 'initial'}]}, @@ -116,6 +201,14 @@ describe('', () => { const wrapper = mountWithApp(); + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); + wrapper.find('button')!.trigger('onClick'); expect(wrapper).toContainReactComponent(MenuGroup, { title: 'updated', @@ -124,6 +217,12 @@ describe('', () => { }); it('updates actions when their lengths change', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [0, 1], + visibleGroups: [], + hiddenActions: [], + hiddenGroups: [], + }); function ActionsWithToggle() { const initialActions: ActionMenuProps['actions'] = [ {content: 'initial'}, @@ -145,6 +244,14 @@ describe('', () => { const wrapper = mountWithApp(); + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); + expect(wrapper).toContainReactComponentTimes(SecondaryAction, 1); wrapper.find('button')!.trigger('onClick'); diff --git a/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts b/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts new file mode 100644 index 00000000000..c034f45fb7d --- /dev/null +++ b/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts @@ -0,0 +1,121 @@ +import {getVisibleAndHiddenActionsIndices} from '../utilities'; + +describe('getVisibleAndHiddenActionsIndices', () => { + const actions = ['Action 1', 'Action 2', 'Action 3']; + const groups = ['Group 1', 'Group 2']; + const disclosureWidth = 20; + const actionsWidths = [50, 60, 70]; + const containerWidth = 200; + + it('should return all actions and groups as visible when container width is greater than the sum of tab widths', () => { + const result = getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + actionsWidths, + containerWidth, + ); + + expect(result.visibleActions).toStrictEqual([0, 1, 2]); + expect(result.hiddenActions).toStrictEqual([]); + expect(result.visibleGroups).toStrictEqual([0, 1]); + expect(result.hiddenGroups).toStrictEqual([]); + }); + + it('should hide actions and groups that exceed the container width', () => { + const customContainerWidth = 100; + const result = getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + actionsWidths, + customContainerWidth, + ); + + expect(result.visibleActions).toStrictEqual([0]); + expect(result.hiddenActions).toStrictEqual([1, 2]); + expect(result.visibleGroups).toStrictEqual([0]); + expect(result.hiddenGroups).toStrictEqual([1]); + }); + + it('should hide all actions and groups when container width is less than the width of the first action', () => { + const customContainerWidth = 40; + const result = getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + actionsWidths, + customContainerWidth, + ); + + expect(result.visibleActions).toStrictEqual([]); + expect(result.hiddenActions).toStrictEqual([0, 1, 2]); + expect(result.visibleGroups).toStrictEqual([]); + expect(result.hiddenGroups).toStrictEqual([0, 1]); + }); + + it('should return empty arrays for visible and hidden actions/groups when actions and groups are empty', () => { + const emptyActions: string[] = []; + const emptyGroups: string[] = []; + const result = getVisibleAndHiddenActionsIndices( + emptyActions, + emptyGroups, + disclosureWidth, + actionsWidths, + containerWidth, + ); + + expect(result.visibleActions).toStrictEqual([]); + expect(result.hiddenActions).toStrictEqual([]); + expect(result.visibleGroups).toStrictEqual([]); + expect(result.hiddenGroups).toStrictEqual([]); + }); + + it('should return empty arrays for visible and hidden actions/groups when actionsWidths is empty', () => { + const emptyActionsWidths: number[] = []; + const result = getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + emptyActionsWidths, + containerWidth, + ); + + expect(result.visibleActions).toStrictEqual([]); + expect(result.hiddenActions).toStrictEqual([]); + expect(result.visibleGroups).toStrictEqual([]); + expect(result.hiddenGroups).toStrictEqual([]); + }); + + it('should hide all actions and groups when container width is less than the width of the first action and disclosureWidth', () => { + const customContainerWidth = 10; + const result = getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + actionsWidths, + customContainerWidth, + ); + + expect(result.visibleActions).toStrictEqual([]); + expect(result.hiddenActions).toStrictEqual([0, 1, 2]); + expect(result.visibleGroups).toStrictEqual([]); + expect(result.hiddenGroups).toStrictEqual([0, 1]); + }); + + it('should hide all actions and groups when actionsWidths is larger than container width', () => { + const customActionsWidths = [300, 400, 500]; + const result = getVisibleAndHiddenActionsIndices( + actions, + groups, + disclosureWidth, + customActionsWidths, + containerWidth, + ); + + expect(result.visibleActions).toStrictEqual([]); + expect(result.hiddenActions).toStrictEqual([0, 1, 2]); + expect(result.visibleGroups).toStrictEqual([]); + expect(result.hiddenGroups).toStrictEqual([0, 1]); + }); +}); diff --git a/polaris-react/src/components/ActionMenu/components/Actions/utilities.ts b/polaris-react/src/components/ActionMenu/components/Actions/utilities.ts new file mode 100644 index 00000000000..3ddfefbdc57 --- /dev/null +++ b/polaris-react/src/components/ActionMenu/components/Actions/utilities.ts @@ -0,0 +1,65 @@ +export function getVisibleAndHiddenActionsIndices( + actions: any[], + groups: any[], + disclosureWidth: number, + actionsWidths: number[], + containerWidth: number, +) { + const sumTabWidths = actionsWidths.reduce((sum, width) => sum + width, 0); + const arrayOfActionsIndices = actions.map((_, index) => { + return index; + }); + const arrayOfGroupsIndices = groups.map((_, index) => { + return index; + }); + + const visibleActions: number[] = []; + const hiddenActions: number[] = []; + const visibleGroups: number[] = []; + const hiddenGroups: number[] = []; + + if (containerWidth > sumTabWidths) { + visibleActions.push(...arrayOfActionsIndices); + visibleGroups.push(...arrayOfGroupsIndices); + } else { + let accumulatedWidth = 0; + + arrayOfActionsIndices.forEach((currentActionsIndex) => { + const currentActionsWidth = actionsWidths[currentActionsIndex]; + + if ( + accumulatedWidth + currentActionsWidth >= + containerWidth - disclosureWidth + ) { + hiddenActions.push(currentActionsIndex); + return; + } + + visibleActions.push(currentActionsIndex); + accumulatedWidth += currentActionsWidth; + }); + + arrayOfGroupsIndices.forEach((currentGroupsIndex) => { + const currentActionsWidth = + actionsWidths[currentGroupsIndex + actions.length]; + + if ( + accumulatedWidth + currentActionsWidth >= + containerWidth - disclosureWidth + ) { + hiddenGroups.push(currentGroupsIndex); + return; + } + + visibleGroups.push(currentGroupsIndex); + accumulatedWidth += currentActionsWidth; + }); + } + + return { + visibleActions, + hiddenActions, + visibleGroups, + hiddenGroups, + }; +} diff --git a/polaris-react/src/components/ActionMenu/components/SecondaryAction/SecondaryAction.tsx b/polaris-react/src/components/ActionMenu/components/SecondaryAction/SecondaryAction.tsx index 8c620e13544..f1fe149a371 100644 --- a/polaris-react/src/components/ActionMenu/components/SecondaryAction/SecondaryAction.tsx +++ b/polaris-react/src/components/ActionMenu/components/SecondaryAction/SecondaryAction.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef} from 'react'; +import React from 'react'; import {classNames} from '../../../../utilities/css'; import {Tooltip} from '../../../Tooltip'; @@ -11,7 +11,6 @@ interface SecondaryAction extends ButtonProps { helpText?: React.ReactNode; destructive?: boolean; onAction?(): void; - getOffsetWidth?(width: number): void; } export function SecondaryAction({ @@ -19,18 +18,9 @@ export function SecondaryAction({ tone, helpText, onAction, - getOffsetWidth, destructive, ...rest }: SecondaryAction) { - const secondaryActionsRef = useRef(null); - - useEffect(() => { - if (!getOffsetWidth || !secondaryActionsRef.current) return; - - getOffsetWidth(secondaryActionsRef.current?.offsetWidth); - }, [getOffsetWidth]); - const buttonMarkup = (
diff --git a/polaris-react/src/components/Page/Page.stories.tsx b/polaris-react/src/components/Page/Page.stories.tsx index 6faf52c45bb..8ee32fef4fb 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, useCallback} from 'react'; import type {ComponentMeta} from '@storybook/react'; import { DeleteIcon, @@ -324,6 +324,65 @@ export function WithActionGroups() { ); } +export function WithActionGroupsAndActions() { + return ( + {}, + }, + { + content: 'Confirm', + onAction: () => {}, + }, + { + content: 'Localize', + url: '/store/marcs-staffed-store/apps/translate-and-adapt/localize/email_template?id=10774151224&locale=fr', + }, + { + content: 'Manage payment reminders', + url: '/store/marcs-staffed-store/settings/notifications/payment_reminders', + }, + ]} + actionGroups={[ + { + title: 'Copy', + onClick: (openActions) => { + console.log('Copy action'); + openActions(); + }, + actions: [{content: 'Copy to clipboard'}], + }, + { + title: 'Promote', + disabled: true, + actions: [{content: 'Share on Facebook'}], + }, + { + title: 'Delete', + disabled: false, + actions: [{content: 'Delete or remove'}], + }, + { + title: 'Other actions', + actions: [ + {content: 'Duplicate'}, + {content: 'Print'}, + {content: 'Unarchive'}, + {content: 'Cancel order'}, + ], + }, + ]} + > + +

Credit card information

+
+
+ ); +} + export function WithContentAfterTitle() { return ( ); } + +export function AsPaymentReminder() { + return ( +
+ {}}} + secondaryActions={[ + { + content: 'Send test', + onAction: () => {}, + }, + { + content: 'Localize', + url: '/store/marcs-staffed-store/apps/translate-and-adapt/localize/email_template?id=10774151224&locale=fr', + }, + { + content: 'Manage payment reminders', + url: '/store/marcs-staffed-store/settings/notifications/payment_reminders', + }, + ]} + > + +

Credit card information

+
+
+
+ ); +} + +export function ActionsWithToggle() { + const initialActions = [{content: 'initial'}]; + + const [actions, setActions] = useState(initialActions); + const handleActivatorClick = useCallback( + () => setActions([{content: 'updated'}]), + [], + ); + + return ( + <> + + + + + + + ); +} From 82467a4e38d5aee1ef3372a56b09359296be730e Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Fri, 12 Jan 2024 14:07:54 +0000 Subject: [PATCH 2/6] chore: add tests for new funtionality --- .../ActionsMeasurer/ActionsMeasurer.tsx | 1 - .../components/Actions/tests/Actions.test.tsx | 130 ++++++++---------- .../Actions/tests/utilities.test.ts | 65 +++------ .../components/MenuGroup/MenuGroup.tsx | 12 -- .../ActionMenu/tests/ActionMenu.test.tsx | 74 +++++++++- 5 files changed, 143 insertions(+), 139 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx index 800ba40fa4a..6d1bf68d83f 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx +++ b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx @@ -72,7 +72,6 @@ export function ActionsMeasurer({ }, [handleMeasurementProp]); useEffect(() => { - console.log('should fire handle measurement'); handleMeasurement(); }, [handleMeasurement, actions, groups]); diff --git a/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx b/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx index 521e4904290..30d579d4198 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx +++ b/polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useState} from 'react'; +import type {CustomRoot} from 'tests/utilities'; import {mountWithApp} from 'tests/utilities'; import {ActionMenu} from '../../..'; @@ -50,13 +51,16 @@ describe('', () => { }); describe('Actions', () => { - it('renders SecondaryActions', () => { + beforeEach(() => { mockGetVisibleAndHiddenActionsIndices({ visibleActions: [0, 1, 2], - visibleGroups: [], + visibleGroups: [0, 1, 2], hiddenActions: [], hiddenGroups: [], }); + }); + + it('renders SecondaryActions', () => { const actionsBeforeOverriddenOrder: ActionMenuProps['actions'] = [ {content: 'mock content 0'}, {content: 'mock content 1'}, @@ -67,24 +71,12 @@ describe('', () => { , ); - wrapper.act(() => { - wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { - containerWidth: 100, - disclosureWidth: 100, - hiddenActionsWidths: [100], - }); - }); + forceMeasurement(wrapper); expect(wrapper.findAll(SecondaryAction)).toHaveLength(3); }); it('renders a when helpText is set on an action', () => { - mockGetVisibleAndHiddenActionsIndices({ - visibleActions: [0], - visibleGroups: [], - hiddenActions: [], - hiddenGroups: [], - }); const toolTipAction = { content: 'Refund', helpText: @@ -93,13 +85,7 @@ describe('', () => { const wrapper = mountWithApp(); - wrapper.act(() => { - wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { - containerWidth: 100, - disclosureWidth: 100, - hiddenActionsWidths: [100], - }); - }); + forceMeasurement(wrapper); const action = wrapper.find(SecondaryAction); @@ -109,34 +95,16 @@ describe('', () => { }); it('renders a MenuGroup', () => { - mockGetVisibleAndHiddenActionsIndices({ - visibleActions: [], - visibleGroups: [0], - hiddenActions: [], - hiddenGroups: [], - }); const wrapper = mountWithApp( , ); - wrapper.act(() => { - wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { - containerWidth: 100, - disclosureWidth: 100, - hiddenActionsWidths: [100], - }); - }); + forceMeasurement(wrapper); expect(wrapper.findAll(MenuGroup)).toHaveLength(1); }); it('updates actions when they change', () => { - mockGetVisibleAndHiddenActionsIndices({ - visibleActions: [0, 1], - visibleGroups: [], - hiddenActions: [], - hiddenGroups: [], - }); function ActionsWithToggle() { const initialActions: ActionMenuProps['actions'] = [ {content: 'initial'}, @@ -158,13 +126,7 @@ describe('', () => { const wrapper = mountWithApp(); - wrapper.act(() => { - wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { - containerWidth: 100, - disclosureWidth: 100, - hiddenActionsWidths: [100], - }); - }); + forceMeasurement(wrapper); wrapper.find('button')!.trigger('onClick'); expect(wrapper).toContainReactComponent(SecondaryAction, { @@ -173,12 +135,6 @@ describe('', () => { }); it('updates groups when they change', () => { - mockGetVisibleAndHiddenActionsIndices({ - visibleActions: [], - visibleGroups: [0, 1], - hiddenActions: [], - hiddenGroups: [], - }); function ActionsWithToggle() { const initialGroups: ActionMenuProps['groups'] = [ {title: 'initial', actions: [{content: 'initial'}]}, @@ -201,13 +157,7 @@ describe('', () => { const wrapper = mountWithApp(); - wrapper.act(() => { - wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { - containerWidth: 100, - disclosureWidth: 100, - hiddenActionsWidths: [100], - }); - }); + forceMeasurement(wrapper); wrapper.find('button')!.trigger('onClick'); expect(wrapper).toContainReactComponent(MenuGroup, { @@ -217,12 +167,6 @@ describe('', () => { }); it('updates actions when their lengths change', () => { - mockGetVisibleAndHiddenActionsIndices({ - visibleActions: [0, 1], - visibleGroups: [], - hiddenActions: [], - hiddenGroups: [], - }); function ActionsWithToggle() { const initialActions: ActionMenuProps['actions'] = [ {content: 'initial'}, @@ -244,13 +188,7 @@ describe('', () => { const wrapper = mountWithApp(); - wrapper.act(() => { - wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { - containerWidth: 100, - disclosureWidth: 100, - hiddenActionsWidths: [100], - }); - }); + forceMeasurement(wrapper); expect(wrapper).toContainReactComponentTimes(SecondaryAction, 1); @@ -259,4 +197,48 @@ describe('', () => { expect(wrapper).toContainReactComponentTimes(SecondaryAction, 2); }); }); + + it('hides actions when they match the hiddenActions value', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [0, 1], + visibleGroups: [], + hiddenActions: [2], + hiddenGroups: [], + }); + + const wrapper = mountWithApp( + , + ); + + forceMeasurement(wrapper); + + expect(wrapper).toContainReactComponent(SecondaryAction, { + children: 'mock content 0', + }); + expect(wrapper).toContainReactComponent(SecondaryAction, { + children: 'mock content 1', + }); + expect(wrapper).toContainReactComponent(SecondaryAction, { + children: 'More actions', + }); + expect(wrapper).not.toContainReactComponent(SecondaryAction, { + children: 'mock content 2', + }); + }); }); + +function forceMeasurement(wrapper: CustomRoot) { + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); +} diff --git a/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts b/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts index c034f45fb7d..7150e34385d 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts +++ b/polaris-react/src/components/ActionMenu/components/Actions/tests/utilities.test.ts @@ -4,10 +4,10 @@ describe('getVisibleAndHiddenActionsIndices', () => { const actions = ['Action 1', 'Action 2', 'Action 3']; const groups = ['Group 1', 'Group 2']; const disclosureWidth = 20; - const actionsWidths = [50, 60, 70]; - const containerWidth = 200; + const actionsWidths = [50, 60, 70, 80, 90]; + const containerWidth = 400; - it('should return all actions and groups as visible when container width is greater than the sum of tab widths', () => { + it('returns all actions and groups as visible when container width is greater than the sum of tab widths', () => { const result = getVisibleAndHiddenActionsIndices( actions, groups, @@ -22,7 +22,7 @@ describe('getVisibleAndHiddenActionsIndices', () => { expect(result.hiddenGroups).toStrictEqual([]); }); - it('should hide actions and groups that exceed the container width', () => { + it('hides actions and groups that exceed the container width', () => { const customContainerWidth = 100; const result = getVisibleAndHiddenActionsIndices( actions, @@ -34,11 +34,11 @@ describe('getVisibleAndHiddenActionsIndices', () => { expect(result.visibleActions).toStrictEqual([0]); expect(result.hiddenActions).toStrictEqual([1, 2]); - expect(result.visibleGroups).toStrictEqual([0]); - expect(result.hiddenGroups).toStrictEqual([1]); + expect(result.visibleGroups).toStrictEqual([]); + expect(result.hiddenGroups).toStrictEqual([0, 1]); }); - it('should hide all actions and groups when container width is less than the width of the first action', () => { + it('hides all actions and groups when container width is less than the width of the first action', () => { const customContainerWidth = 40; const result = getVisibleAndHiddenActionsIndices( actions, @@ -54,57 +54,24 @@ describe('getVisibleAndHiddenActionsIndices', () => { expect(result.hiddenGroups).toStrictEqual([0, 1]); }); - it('should return empty arrays for visible and hidden actions/groups when actions and groups are empty', () => { - const emptyActions: string[] = []; - const emptyGroups: string[] = []; - const result = getVisibleAndHiddenActionsIndices( - emptyActions, - emptyGroups, - disclosureWidth, - actionsWidths, - containerWidth, - ); - - expect(result.visibleActions).toStrictEqual([]); - expect(result.hiddenActions).toStrictEqual([]); - expect(result.visibleGroups).toStrictEqual([]); - expect(result.hiddenGroups).toStrictEqual([]); - }); - - it('should return empty arrays for visible and hidden actions/groups when actionsWidths is empty', () => { - const emptyActionsWidths: number[] = []; + it('will show one action and one group if the other action widths do not fit', () => { + const customActionWidths = [50, 400, 400, 60, 350]; const result = getVisibleAndHiddenActionsIndices( actions, groups, disclosureWidth, - emptyActionsWidths, + customActionWidths, containerWidth, ); - expect(result.visibleActions).toStrictEqual([]); - expect(result.hiddenActions).toStrictEqual([]); - expect(result.visibleGroups).toStrictEqual([]); - expect(result.hiddenGroups).toStrictEqual([]); - }); - - it('should hide all actions and groups when container width is less than the width of the first action and disclosureWidth', () => { - const customContainerWidth = 10; - const result = getVisibleAndHiddenActionsIndices( - actions, - groups, - disclosureWidth, - actionsWidths, - customContainerWidth, - ); - - expect(result.visibleActions).toStrictEqual([]); - expect(result.hiddenActions).toStrictEqual([0, 1, 2]); - expect(result.visibleGroups).toStrictEqual([]); - expect(result.hiddenGroups).toStrictEqual([0, 1]); + expect(result.visibleActions).toStrictEqual([0]); + expect(result.hiddenActions).toStrictEqual([1, 2]); + expect(result.visibleGroups).toStrictEqual([0]); + expect(result.hiddenGroups).toStrictEqual([1]); }); - it('should hide all actions and groups when actionsWidths is larger than container width', () => { - const customActionsWidths = [300, 400, 500]; + it('hides all actions and groups when actionsWidths is larger than container width', () => { + const customActionsWidths = [500, 400, 500, 600, 700]; const result = getVisibleAndHiddenActionsIndices( actions, groups, diff --git a/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx b/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx index 4326d6218e2..0a69e38c96c 100644 --- a/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx +++ b/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx @@ -18,8 +18,6 @@ export interface MenuGroupProps extends MenuGroupDescriptor { onOpen(title: string): void; /** Callback for closing the MenuGroup by title */ onClose(title: string): void; - /** Callback for getting the offsetWidth of the MenuGroup */ - getOffsetWidth?(width: number): void; /** Collection of sectioned action items */ sections?: readonly ActionListSection[]; } @@ -35,7 +33,6 @@ export function MenuGroup({ onClick, onClose, onOpen, - getOffsetWidth, sections, }: MenuGroupProps) { const handleClose = useCallback(() => { @@ -54,14 +51,6 @@ export function MenuGroup({ } }, [onClick, handleOpen]); - const handleOffsetWidth = useCallback( - (width: number) => { - if (!getOffsetWidth) return; - getOffsetWidth(width); - }, - [getOffsetWidth], - ); - const popoverActivator = ( {title} diff --git a/polaris-react/src/components/ActionMenu/tests/ActionMenu.test.tsx b/polaris-react/src/components/ActionMenu/tests/ActionMenu.test.tsx index c63df384eab..618d5e22512 100644 --- a/polaris-react/src/components/ActionMenu/tests/ActionMenu.test.tsx +++ b/polaris-react/src/components/ActionMenu/tests/ActionMenu.test.tsx @@ -1,15 +1,47 @@ import React from 'react'; +import type {CustomRoot} from 'tests/utilities'; import {mountWithApp} from 'tests/utilities'; import type { MenuGroupDescriptor, ActionListItemDescriptor, } from '../../../types'; +// eslint-disable-next-line @shopify/strict-component-boundaries +import type {getVisibleAndHiddenActionsIndices} from '../components/Actions/utilities'; import {MenuGroup, RollupActions, Actions} from '../components'; import {ActionMenu} from '../ActionMenu'; import type {ActionMenuProps} from '../ActionMenu'; import {Button} from '../../Button'; -import {ButtonGroup} from '../../ButtonGroup'; +// eslint-disable-next-line @shopify/strict-component-boundaries +import {ActionsMeasurer} from '../components/Actions/components'; + +jest.mock('../components/Actions/components/ActionsMeasurer', () => ({ + ActionsMeasurer: function ActionsMeasurer() { + return null; + }, +})); + +jest.mock('../components/Actions/utilities', () => ({ + ...jest.requireActual('../components/Actions/utilities'), + getVisibleAndHiddenActionsIndices: jest.fn(), +})); + +function mockGetVisibleAndHiddenActionsIndices( + args: ReturnType, +) { + const getVisibleAndHiddenActionsIndices: jest.Mock = jest.requireMock( + '../components/Actions/utilities', + ).getVisibleAndHiddenActionsIndices; + + getVisibleAndHiddenActionsIndices.mockReturnValue(args); +} + +const mockAllVisible = { + visibleActions: [0, 1], + visibleGroups: [0, 1], + hiddenActions: [], + hiddenGroups: [], +}; describe('', () => { const mockProps: ActionMenuProps = { @@ -23,6 +55,10 @@ describe('', () => { {content: 'mock content 2'}, ]; + beforeEach(() => { + mockGetVisibleAndHiddenActionsIndices(mockAllVisible); + }); + it('does not render when there are no `actions` or `groups`', () => { const wrapper = mountWithApp(); expect(wrapper.findAll(MenuGroup)).toHaveLength(0); @@ -57,15 +93,25 @@ describe('', () => { const wrapper = mountWithApp( , ); + forceMeasurement(wrapper); expect(wrapper.findAll(MenuGroup)).toHaveLength(mockGroups.length); }); it('renders disabled groups when `rollup` is `false`', () => { + mockGetVisibleAndHiddenActionsIndices({ + visibleActions: [0, 1], + visibleGroups: [0, 1, 2], + hiddenActions: [], + hiddenGroups: [], + }); + const wrapper = mountWithApp( , ); + forceMeasurement(wrapper); + expect(wrapper.findAll(MenuGroup)).toHaveLength( mockGroupsWithDisabledGroup.length, ); @@ -113,6 +159,8 @@ describe('', () => { it('renders groups in their initial order when no indexes are set', () => { const wrapper = mountWithApp(); + forceMeasurement(wrapper); + wrapper.findAll(MenuGroup).forEach((group, index) => { expect(group.props).toMatchObject(mockGroups[index]); }); @@ -128,6 +176,8 @@ describe('', () => { const wrapper = mountWithApp(); + forceMeasurement(wrapper); + expect(wrapper.findAll(MenuGroup)).toHaveLength(1); }); }); @@ -145,6 +195,8 @@ describe('', () => { , ); + forceMeasurement(wrapper); + expect(wrapper).toContainReactComponent(MenuGroup, { active: false, }); @@ -157,6 +209,8 @@ describe('', () => { , ); + forceMeasurement(wrapper); + wrapper.find(MenuGroup)!.trigger('onOpen', mockTitle); expect(wrapper).toContainReactComponent(MenuGroup, { @@ -171,6 +225,8 @@ describe('', () => { , ); + forceMeasurement(wrapper); + wrapper.find(MenuGroup)!.trigger('onOpen', mockTitle); wrapper.find(MenuGroup)!.trigger('onClose', mockTitle); @@ -181,13 +237,13 @@ describe('', () => { }); describe('', () => { - it('uses Button and ButtonGroup as subcomponents', () => { + it('uses Button as subcomponents', () => { const wrapper = mountWithApp( , ); + forceMeasurement(wrapper); expect(wrapper.findAll(Button)).toHaveLength(2); - expect(wrapper.findAll(ButtonGroup)).toHaveLength(1); }); it('passes action callbacks through to Button', () => { @@ -199,6 +255,8 @@ describe('', () => { />, ); + forceMeasurement(wrapper); + wrapper.find(Button)!.trigger('onClick'); expect(spy).toHaveBeenCalledTimes(1); @@ -233,3 +291,13 @@ function fillMenuGroup(partialMenuGroup?: Partial) { ...partialMenuGroup, }; } + +function forceMeasurement(wrapper: CustomRoot) { + wrapper.act(() => { + wrapper.find(ActionsMeasurer)!.trigger('handleMeasurement', { + containerWidth: 100, + disclosureWidth: 100, + hiddenActionsWidths: [100], + }); + }); +} From d6bbc4ee53f55a49462484dcd88ae90f0286c11d Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Fri, 12 Jan 2024 14:16:35 +0000 Subject: [PATCH 3/6] changeset --- .changeset/heavy-deers-applaud.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/heavy-deers-applaud.md diff --git a/.changeset/heavy-deers-applaud.md b/.changeset/heavy-deers-applaud.md new file mode 100644 index 00000000000..f9f82fee6d3 --- /dev/null +++ b/.changeset/heavy-deers-applaud.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +[ActionMenu] Improved width calculation logic to show and roll up actions" From b44a19ab2e4f11926dc35fab0adb24851fc24ace Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Fri, 19 Jan 2024 12:58:33 +0000 Subject: [PATCH 4/6] ensure no wrapping of title --- .../ActionsMeasurer/ActionsMeasurer.tsx | 2 +- .../src/components/Page/Page.stories.tsx | 54 ------------------- .../Page/components/Header/Header.module.scss | 1 + 3 files changed, 2 insertions(+), 55 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx index 6d1bf68d83f..68d6622de16 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx +++ b/polaris-react/src/components/ActionMenu/components/Actions/components/ActionsMeasurer/ActionsMeasurer.tsx @@ -54,7 +54,7 @@ export function ActionsMeasurer({ return; } - const containerWidth = containerNode.current.offsetWidth - 20 - 28; + const containerWidth = containerNode.current.offsetWidth; const hiddenActionNodes = containerNode.current.children; const hiddenActionNodesArray = Array.from(hiddenActionNodes); const hiddenActionsWidths = hiddenActionNodesArray.map((node) => { diff --git a/polaris-react/src/components/Page/Page.stories.tsx b/polaris-react/src/components/Page/Page.stories.tsx index 8ee32fef4fb..328a937f2d5 100644 --- a/polaris-react/src/components/Page/Page.stories.tsx +++ b/polaris-react/src/components/Page/Page.stories.tsx @@ -433,57 +433,3 @@ export function WithContentAfterTitleAndSubtitle() {
); } - -export function AsPaymentReminder() { - return ( -
- {}}} - secondaryActions={[ - { - content: 'Send test', - onAction: () => {}, - }, - { - content: 'Localize', - url: '/store/marcs-staffed-store/apps/translate-and-adapt/localize/email_template?id=10774151224&locale=fr', - }, - { - content: 'Manage payment reminders', - url: '/store/marcs-staffed-store/settings/notifications/payment_reminders', - }, - ]} - > - -

Credit card information

-
-
-
- ); -} - -export function ActionsWithToggle() { - const initialActions = [{content: 'initial'}]; - - const [actions, setActions] = useState(initialActions); - const handleActivatorClick = useCallback( - () => setActions([{content: 'updated'}]), - [], - ); - - return ( - <> - - - - - - - ); -} diff --git a/polaris-react/src/components/Page/components/Header/Header.module.scss b/polaris-react/src/components/Page/components/Header/Header.module.scss index c014372f545..9f86e044382 100644 --- a/polaris-react/src/components/Page/components/Header/Header.module.scss +++ b/polaris-react/src/components/Page/components/Header/Header.module.scss @@ -7,6 +7,7 @@ $action-menu-rollup-computed-width: 40px; margin-top: var(--p-space-100); align-self: center; flex: 1 1 auto; + min-width: fit-content; @media (--p-breakpoints-sm-up) { margin-top: 0; From 515a20d61fc2848523ac15c6f65c151df4fdce3f Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Mon, 22 Jan 2024 08:41:06 +0000 Subject: [PATCH 5/6] Update polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx Co-authored-by: Chloe Rice --- .../src/components/ActionMenu/components/Actions/Actions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx b/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx index 3fe9fe55398..8041ebe89c9 100644 --- a/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx +++ b/polaris-react/src/components/ActionMenu/components/Actions/Actions.tsx @@ -75,12 +75,12 @@ export function Actions({actions = [], groups = [], onActionRollup}: Props) { ); }); - const groupsToFilters = + const groupsToFilter = hiddenGroups.length > 0 || hiddenActions.length > 0 ? [...groups, defaultRollupGroup] : [...groups]; - const filteredGroups = groupsToFilters.filter((group, index) => { + const filteredGroups = groupsToFilter.filter((group, index) => { const hasNoGroupsProp = groups.length === 0; const isVisibleGroup = visibleGroups.includes(index); const isDefaultGroup = group === defaultRollupGroup; From 278f2edd008de0cfb56f1f06fb6c6af5a0e89559 Mon Sep 17 00:00:00 2001 From: Marc Thomas Date: Mon, 22 Jan 2024 08:41:16 +0000 Subject: [PATCH 6/6] Update .changeset/heavy-deers-applaud.md Co-authored-by: Chloe Rice --- .changeset/heavy-deers-applaud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/heavy-deers-applaud.md b/.changeset/heavy-deers-applaud.md index f9f82fee6d3..bde7a07de50 100644 --- a/.changeset/heavy-deers-applaud.md +++ b/.changeset/heavy-deers-applaud.md @@ -2,4 +2,4 @@ '@shopify/polaris': minor --- -[ActionMenu] Improved width calculation logic to show and roll up actions" +Improved the logic of action rollup and calculation of available space in `ActionMenu` and `Tabs`