Add vertical alignment to the GroupBlock

closes RCX-2291
flag=block_editor

test plan:
  - start a page from scratch
  - change the layout from column to row on the default Group
  - add a couple things (icons will do)
  > expect them to be in a row
  - add a group
  - ad a couple things to that group
  > expect the new group items to be in a column
  > expect the original items to be at the top of the Group
  - on the outer group, change the vertical alignment to center
  > expect the original items to be cenered vertically
  - change the vertical alignment to end
  > expect the original items to be at the bottom

Change-Id: I39c249a3800b95ee1dd2e03d58a5a47fd7e31861
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/355890
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jacob DeWar <jacob.dewar@instructure.com>
QA-Review: Jacob DeWar <jacob.dewar@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Ed Schiebel 2024-08-24 12:39:10 -06:00
parent 334a04e585
commit 7c8cf65ff4
6 changed files with 193 additions and 32 deletions

View File

@ -33,6 +33,7 @@ const I18n = useI18nScope('block-editor')
export const GroupBlock = (props: GroupBlockProps) => { export const GroupBlock = (props: GroupBlockProps) => {
const { const {
alignment = GroupBlock.craft.defaultProps.alignment, alignment = GroupBlock.craft.defaultProps.alignment,
verticalAlignment = GroupBlock.craft.defaultProps.verticalAlignment,
layout = GroupBlock.craft.defaultProps.layout, layout = GroupBlock.craft.defaultProps.layout,
resizable = GroupBlock.craft.defaultProps.resizable, resizable = GroupBlock.craft.defaultProps.resizable,
} = props } = props
@ -44,6 +45,7 @@ export const GroupBlock = (props: GroupBlockProps) => {
'group-block', 'group-block',
`${layout}-layout`, `${layout}-layout`,
`${alignment}-align`, `${alignment}-align`,
`${verticalAlignment}-valign`,
]) ])
const {node} = useNode((n: Node) => { const {node} = useNode((n: Node) => {
return { return {
@ -76,6 +78,7 @@ GroupBlock.craft = {
displayName: I18n.t('Group'), displayName: I18n.t('Group'),
defaultProps: { defaultProps: {
alignment: 'start', alignment: 'start',
verticalAlignment: 'start',
layout: 'column', layout: 'column',
resizable: true, resizable: true,
}, },

View File

@ -60,15 +60,33 @@ export const GroupBlockToolbar = () => {
[setProp] [setProp]
) )
const renderAlignmentIcon = () => { const handleChangeVerticalAlignment = useCallback(
switch (props.alignment) { (e, value) => {
setProp((prps: GroupBlockProps) => {
prps.verticalAlignment = value as GroupAlignment
})
},
[setProp]
)
const rotate = {
rotate: '90deg',
}
const renderAlignmentIcon = (vertical: boolean) => {
let icon
const align = vertical ? props.verticalAlignment : props.alignment
switch (align) {
case 'start': case 'start':
return <IconTextStartLine size="x-small" /> icon = <IconTextStartLine size="x-small" />
break
case 'center': case 'center':
return <IconTextCenteredLine size="x-small" /> icon = <IconTextCenteredLine size="x-small" />
break
case 'end': case 'end':
return <IconTextEndLine size="x-small" /> icon = <IconTextEndLine size="x-small" />
} }
return vertical ? <span style={rotate}>{icon}</span> : icon
} }
return ( return (
@ -93,15 +111,16 @@ export const GroupBlockToolbar = () => {
{I18n.t('Row')} {I18n.t('Row')}
</Menu.Item> </Menu.Item>
</Menu> </Menu>
<Menu <Menu
trigger={ trigger={
<IconButton <IconButton
size="small" size="small"
withBorder={false} withBorder={false}
withBackground={false} withBackground={false}
screenReaderLabel={I18n.t('Align')} screenReaderLabel={I18n.t('Align Horizontally')}
> >
{renderAlignmentIcon()} {renderAlignmentIcon(false)}
</IconButton> </IconButton>
} }
onSelect={handleChangeAlignment} onSelect={handleChangeAlignment}
@ -112,19 +131,66 @@ export const GroupBlockToolbar = () => {
<Text>{I18n.t('Align to start')}</Text> <Text>{I18n.t('Align to start')}</Text>
</Flex> </Flex>
</Menu.Item> </Menu.Item>
<Menu.Item type="checkbox" value="center" defaultSelected={props.layout === 'center'}> <Menu.Item type="checkbox" value="center" defaultSelected={props.alignment === 'center'}>
<Flex gap="x-small"> <Flex gap="x-small">
<IconTextCenteredLine size="x-small" /> <IconTextCenteredLine size="x-small" />
<Text>{I18n.t('Align to center')}</Text> <Text>{I18n.t('Align to center')}</Text>
</Flex> </Flex>
</Menu.Item> </Menu.Item>
<Menu.Item type="checkbox" value="end" defaultSelected={props.layout === 'end'}> <Menu.Item type="checkbox" value="end" defaultSelected={props.alignment === 'end'}>
<Flex gap="x-small"> <Flex gap="x-small">
<IconTextEndLine size="x-small" /> <IconTextEndLine size="x-small" />
<Text>{I18n.t('Align to end')}</Text> <Text>{I18n.t('Align to end')}</Text>
</Flex> </Flex>
</Menu.Item> </Menu.Item>
</Menu> </Menu>
<Menu
trigger={
<IconButton
size="small"
withBorder={false}
withBackground={false}
screenReaderLabel={I18n.t('Align Vertically')}
>
{renderAlignmentIcon(true)}
</IconButton>
}
onSelect={handleChangeVerticalAlignment}
>
<Menu.Item
type="checkbox"
value="start"
defaultSelected={props.verticalAlignment === 'start'}
>
<Flex gap="x-small">
<span style={rotate}>
<IconTextStartLine size="x-small" />
</span>
<Text>{I18n.t('Align to start')}</Text>
</Flex>
</Menu.Item>
<Menu.Item
type="checkbox"
value="center"
defaultSelected={props.verticalAlignment === 'center'}
>
<Flex gap="x-small">
<span style={rotate}>
<IconTextCenteredLine size="x-small" />
</span>
<Text>{I18n.t('Align to center')}</Text>
</Flex>
</Menu.Item>
<Menu.Item type="checkbox" value="end" defaultSelected={props.verticalAlignment === 'end'}>
<Flex gap="x-small">
<span style={rotate}>
<IconTextEndLine size="x-small" />
</span>
<Text>{I18n.t('Align to end')}</Text>
</Flex>
</Menu.Item>
</Menu>
</Flex> </Flex>
) )
} }

View File

@ -63,4 +63,16 @@ describe('ColumnsSection', () => {
expect(container.querySelector('.group-block')).toBeInTheDocument() expect(container.querySelector('.group-block')).toBeInTheDocument()
expect(container.querySelector('.group-block')).toHaveClass('row-layout') expect(container.querySelector('.group-block')).toHaveClass('row-layout')
}) })
it('should render with center horizontal alignment', () => {
const {container} = renderBlock({alignment: 'center'})
expect(container.querySelector('.group-block')).toBeInTheDocument()
expect(container.querySelector('.group-block')).toHaveClass('center-align')
})
it('should render with center vertical alignment', () => {
const {container} = renderBlock({verticalAlignment: 'center'})
expect(container.querySelector('.group-block')).toBeInTheDocument()
expect(container.querySelector('.group-block')).toHaveClass('center-valign')
})
}) })

View File

@ -49,34 +49,104 @@ describe('GroupBlockToolbar', () => {
const {getByText} = render(<GroupBlockToolbar />) const {getByText} = render(<GroupBlockToolbar />)
expect(getByText('Layout direction')).toBeInTheDocument() expect(getByText('Layout direction')).toBeInTheDocument()
expect(getByText('Align Horizontally')).toBeInTheDocument()
expect(getByText('Align Vertically')).toBeInTheDocument()
}) })
it('checks the right layout direction', async () => { describe('layout direction', () => {
const {getByText} = render(<GroupBlockToolbar />) it('checks the right layout direction', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Layout direction').closest('button') as HTMLButtonElement const btn = getByText('Layout direction').closest('button') as HTMLButtonElement
await userEvent.click(btn) await userEvent.click(btn)
const colMenuItem = screen.getByText('Column') const colMenuItem = screen.getByText('Column')
const rowMenuItem = screen.getByText('Row') const rowMenuItem = screen.getByText('Row')
expect(colMenuItem).toBeInTheDocument() expect(colMenuItem).toBeInTheDocument()
expect(rowMenuItem).toBeInTheDocument() expect(rowMenuItem).toBeInTheDocument()
const li = colMenuItem.closest('li') as HTMLLIElement const li = colMenuItem.closest('li') as HTMLLIElement
expect(li.querySelector('svg[name="IconCheck"]')).toBeInTheDocument() expect(li.querySelector('svg[name="IconCheck"]')).toBeInTheDocument()
})
it('changes the direction prop', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Layout direction').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const rowMenuItem = screen.getByText('Row')
await userEvent.click(rowMenuItem)
expect(mockSetProp).toHaveBeenCalled()
expect(props.layout).toBe('row')
})
}) })
it('changes the direction prop', async () => { describe('horizontal alignment', () => {
const {getByText} = render(<GroupBlockToolbar />) it('checks the right alignment', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Layout direction').closest('button') as HTMLButtonElement const btn = getByText('Align Horizontally').closest('button') as HTMLButtonElement
await userEvent.click(btn) await userEvent.click(btn)
const rowMenuItem = screen.getByText('Row') const startMenuItem = screen.getByText('Align to start')
await userEvent.click(rowMenuItem) const centerMenuItem = screen.getByText('Align to center')
const endMenuItem = screen.getByText('Align to end')
expect(mockSetProp).toHaveBeenCalled() expect(startMenuItem).toBeInTheDocument()
expect(props.layout).toBe('row') expect(centerMenuItem).toBeInTheDocument()
expect(endMenuItem).toBeInTheDocument()
const startLi = startMenuItem.closest('li') as HTMLLIElement
expect(startLi.querySelector('svg[name="IconCheck"]')).toBeInTheDocument()
})
it('changes the alignment prop', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Align Horizontally').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const centerMenuItem = screen.getByText('Align to center')
await userEvent.click(centerMenuItem)
expect(mockSetProp).toHaveBeenCalled()
expect(props.alignment).toBe('center')
})
})
describe('vertical alignment', () => {
it('checks the right alignment', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Align Vertically').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const startMenuItem = screen.getByText('Align to start')
const centerMenuItem = screen.getByText('Align to center')
const endMenuItem = screen.getByText('Align to end')
expect(startMenuItem).toBeInTheDocument()
expect(centerMenuItem).toBeInTheDocument()
expect(endMenuItem).toBeInTheDocument()
const startLi = startMenuItem.closest('li') as HTMLLIElement
expect(startLi.querySelector('svg[name="IconCheck"]')).toBeInTheDocument()
})
it('changes the vertical alignment prop', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Align Vertically').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const centerMenuItem = screen.getByText('Align to center')
await userEvent.click(centerMenuItem)
expect(mockSetProp).toHaveBeenCalled()
expect(props.verticalAlignment).toBe('center')
})
}) })
}) })

View File

@ -22,5 +22,6 @@ export type GroupAlignment = 'start' | 'center' | 'end'
export type GroupBlockProps = { export type GroupBlockProps = {
layout?: GroupLayout layout?: GroupLayout
alignment?: GroupAlignment alignment?: GroupAlignment
verticalAlignment?: GroupAlignment
resizable?: boolean resizable?: boolean
} }

View File

@ -312,22 +312,31 @@
flex-direction: column; flex-direction: column;
} }
&.row-layout { &.row-layout {
&> .no-sections { &> .group-block__inner {
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
} }
&.center-align > .no-sections { &.center-align > .group-block__inner {
justify-content: center; justify-content: center;
} }
&.end-align > .no-sections { &.end-align > .group-block__inner {
justify-content: flex-end; justify-content: flex-end;
} }
&.start-valign > .group-block__inner {
align-items: flex-start;
}
&.center-valign > .group-block__inner {
align-items: center;
}
&.end-valign > .group-block__inner {
align-items: flex-end;
}
} }
&.column-layout { &.column-layout {
&.center-align > .no-sections { &.center-align > .group-block__inner {
align-items: center; align-items: center;
} }
&.end-align > .no-sections { &.end-align > .group-block__inner {
align-items: flex-end; align-items: flex-end;
} }
} }