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:
parent
334a04e585
commit
7c8cf65ff4
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue