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

View File

@ -60,15 +60,33 @@ export const GroupBlockToolbar = () => {
[setProp]
)
const renderAlignmentIcon = () => {
switch (props.alignment) {
const handleChangeVerticalAlignment = useCallback(
(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':
return <IconTextStartLine size="x-small" />
icon = <IconTextStartLine size="x-small" />
break
case 'center':
return <IconTextCenteredLine size="x-small" />
icon = <IconTextCenteredLine size="x-small" />
break
case 'end':
return <IconTextEndLine size="x-small" />
icon = <IconTextEndLine size="x-small" />
}
return vertical ? <span style={rotate}>{icon}</span> : icon
}
return (
@ -93,15 +111,16 @@ export const GroupBlockToolbar = () => {
{I18n.t('Row')}
</Menu.Item>
</Menu>
<Menu
trigger={
<IconButton
size="small"
withBorder={false}
withBackground={false}
screenReaderLabel={I18n.t('Align')}
screenReaderLabel={I18n.t('Align Horizontally')}
>
{renderAlignmentIcon()}
{renderAlignmentIcon(false)}
</IconButton>
}
onSelect={handleChangeAlignment}
@ -112,19 +131,66 @@ export const GroupBlockToolbar = () => {
<Text>{I18n.t('Align to start')}</Text>
</Flex>
</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">
<IconTextCenteredLine size="x-small" />
<Text>{I18n.t('Align to center')}</Text>
</Flex>
</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">
<IconTextEndLine size="x-small" />
<Text>{I18n.t('Align to end')}</Text>
</Flex>
</Menu.Item>
</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>
)
}

View File

@ -63,4 +63,16 @@ describe('ColumnsSection', () => {
expect(container.querySelector('.group-block')).toBeInTheDocument()
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 />)
expect(getByText('Layout direction')).toBeInTheDocument()
expect(getByText('Align Horizontally')).toBeInTheDocument()
expect(getByText('Align Vertically')).toBeInTheDocument()
})
it('checks the right layout direction', async () => {
const {getByText} = render(<GroupBlockToolbar />)
describe('layout direction', () => {
it('checks the right layout direction', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Layout direction').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const btn = getByText('Layout direction').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const colMenuItem = screen.getByText('Column')
const rowMenuItem = screen.getByText('Row')
const colMenuItem = screen.getByText('Column')
const rowMenuItem = screen.getByText('Row')
expect(colMenuItem).toBeInTheDocument()
expect(rowMenuItem).toBeInTheDocument()
expect(colMenuItem).toBeInTheDocument()
expect(rowMenuItem).toBeInTheDocument()
const li = colMenuItem.closest('li') as HTMLLIElement
expect(li.querySelector('svg[name="IconCheck"]')).toBeInTheDocument()
const li = colMenuItem.closest('li') as HTMLLIElement
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 () => {
const {getByText} = render(<GroupBlockToolbar />)
describe('horizontal alignment', () => {
it('checks the right alignment', async () => {
const {getByText} = render(<GroupBlockToolbar />)
const btn = getByText('Layout direction').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const btn = getByText('Align Horizontally').closest('button') as HTMLButtonElement
await userEvent.click(btn)
const rowMenuItem = screen.getByText('Row')
await userEvent.click(rowMenuItem)
const startMenuItem = screen.getByText('Align to start')
const centerMenuItem = screen.getByText('Align to center')
const endMenuItem = screen.getByText('Align to end')
expect(mockSetProp).toHaveBeenCalled()
expect(props.layout).toBe('row')
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 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 = {
layout?: GroupLayout
alignment?: GroupAlignment
verticalAlignment?: GroupAlignment
resizable?: boolean
}

View File

@ -312,22 +312,31 @@
flex-direction: column;
}
&.row-layout {
&> .no-sections {
&> .group-block__inner {
flex-direction: row;
flex-wrap: wrap;
}
&.center-align > .no-sections {
&.center-align > .group-block__inner {
justify-content: center;
}
&.end-align > .no-sections {
&.end-align > .group-block__inner {
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 {
&.center-align > .no-sections {
&.center-align > .group-block__inner {
align-items: center;
}
&.end-align > .no-sections {
&.end-align > .group-block__inner {
align-items: flex-end;
}
}