From 430e0b0560ffcd18204228e794b6df020dda3e77 Mon Sep 17 00:00:00 2001 From: arthur791004 Date: Tue, 24 Oct 2023 18:37:29 +0800 Subject: [PATCH] Assembler: Allow to zoom out the preview (#83034) * Device Switcher: Add range control of zoom * Device Switcher: Handle zoom-out * Device Switcher: Adjust styles * Adjust the zoom-out behavior * Adjust slider styles * Adjust styles * Disable zoom-out behavior when there is no selected pattern * Clean up: PatternLayout * Keep the action bar un-scaled and stick to the top of the active pattern * Centralize viewport components * Adjust action bar position * Fix types * Fix viewport height * Add tracks event * Debounce the tracks event --- .../pattern-assembler/events.ts | 4 +- .../pattern-assembler/index.tsx | 1 - .../pattern-assembler/pattern-action-bar.scss | 112 ++++++++----- .../pattern-assembler/pattern-action-bar.tsx | 12 +- .../pattern-large-preview.scss | 48 +----- .../pattern-large-preview.tsx | 155 ++++++++++++----- .../pattern-assembler/pattern-layout.tsx | 80 --------- .../pattern-assembler/style.scss | 158 ------------------ .../src/device-switcher/device-switcher.scss | 71 ++++++++ .../src/device-switcher/device-switcher.tsx | 57 +++++-- .../src/device-switcher/toolbar.scss | 2 +- .../src/device-switcher/use-zoom-out.ts | 34 ++++ 12 files changed, 358 insertions(+), 376 deletions(-) delete mode 100644 client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-layout.tsx create mode 100644 packages/components/src/device-switcher/use-zoom-out.ts diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/events.ts b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/events.ts index f3b193d44ad7c..c4d14b99f53c3 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/events.ts +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/events.ts @@ -64,6 +64,6 @@ export const PATTERN_ASSEMBLER_EVENTS = { /** * Large Preview */ - LARGE_PREVIEW_ADD_HEADER_BUTTON_CLICK: - 'calypso_signup_pattern_assembler_large_preview_add_header_button_click', + LARGE_PREVIEW_ZOOM_OUT_SCALE_CHANGE: + 'calypso_signup_pattern_assembler_large_preview_zoom_out_scale_change', } as const; diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/index.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/index.tsx index 005f212f2c807..b79880b3c5cb6 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/index.tsx @@ -628,7 +628,6 @@ const PatternAssembler = ( props: StepProps & NoticesProps ) => { return ( .pattern-action-bar__action:not(:first-child) { + border-left: 1px solid #1e1e1e; + } - &--shuffle { - &.has-icon { - flex-direction: row; - gap: 4px; - line-height: 40px; - max-width: none; - padding: 0 12px; - } - } - } + /** + * Increase the area of the action bar to keep the element active + * when hovering on the the action bar + */ + &::before { + content: ""; + display: none; + position: absolute; + top: -12px; + left: -12px; + width: 100%; + height: 100%; + padding: 12px 12px 20px 12px; + box-sizing: content-box; + z-index: -1; } } diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-action-bar.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-action-bar.tsx index ea1162cdaefdb..c84594345d565 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-action-bar.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-action-bar.tsx @@ -1,6 +1,8 @@ import { Button } from '@wordpress/components'; import { chevronUp, chevronDown, edit, shuffle, trash } from '@wordpress/icons'; +import classnames from 'classnames'; import { useTranslate } from 'i18n-calypso'; +import React from 'react'; import { recordTracksEvent } from 'calypso/lib/analytics/tracks'; import { PATTERN_ASSEMBLER_EVENTS } from './events'; import type { Category } from './types'; @@ -12,11 +14,13 @@ type PatternActionBarProps = { onMoveUp?: () => void; onMoveDown?: () => void; onShuffle: () => void; + onMouseLeave?: ( event: React.MouseEvent< HTMLElement > ) => void; disableMoveUp?: boolean; disableMoveDown?: boolean; patternType: string; category?: Category; source: 'list' | 'large_preview'; + isOverflow?: boolean; }; const PatternActionBar = ( { @@ -25,11 +29,13 @@ const PatternActionBar = ( { onMoveUp, onMoveDown, onShuffle, + onMouseLeave, disableMoveUp, disableMoveDown, patternType, category, source, + isOverflow, }: PatternActionBarProps ) => { const translate = useTranslate(); const eventProps = { @@ -39,10 +45,14 @@ const PatternActionBar = ( { }; return ( + // eslint-disable-next-line jsx-a11y/interactive-supports-focus
{ onMoveUp && onMoveDown && (
diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.scss b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.scss index cf63fd13ef461..43f3af833416b 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.scss +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.scss @@ -37,45 +37,6 @@ $pattern-large-preview-outer-border-radius: calc(var(--device-switcher-border-radius) - var(--device-switcher-border-width)); position: relative; - .pattern-action-bar { - box-sizing: border-box; - position: absolute; - top: 16px; - left: 16px; - right: unset; - padding: 0; - border: 1px solid #1e1e1e; - border-radius: 2px; - gap: 0; - background-color: #fff; - opacity: 0; - z-index: 1; - // Scale up the action bar in fixed viewport - transform: scale(calc(1 / var(--viewport-scale))); - transform-origin: top left; - - > .pattern-action-bar__action { - position: relative; - min-height: 40px; - min-width: 40px; - padding: 2px; - - &:first-child::after { - content: none; - } - - &::after { - background-color: #1e1e1e; - bottom: 0; - content: ""; - left: 0; - position: absolute; - top: 0; - width: 1px; - } - } - } - &::after { content: ""; position: absolute; @@ -96,16 +57,13 @@ border-bottom-right-radius: $pattern-large-preview-outer-border-radius; } + &--active, &:hover, &:focus, &:focus-within { - .pattern-action-bar { - opacity: 1; - } - &::after { - // Scale up the border of a hovered pattern in fixed viewport - border: calc(2px * (1 / var(--viewport-scale))) solid var(--color-primary-light); + // Scale up the border of a active pattern + border: calc(2px * (1 / var(--viewport-scale) / var(--pattern-large-preview-zoom-out-scale))) solid var(--color-primary-light); } } } diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.tsx index 8c38f039d7201..d5f18c97cf9b9 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/pattern-large-preview.tsx @@ -1,9 +1,11 @@ import { PatternRenderer } from '@automattic/block-renderer'; import { DeviceSwitcher } from '@automattic/components'; import { useGlobalStyle } from '@automattic/global-styles'; +import { Popover } from '@wordpress/components'; import classnames from 'classnames'; import { useTranslate } from 'i18n-calypso'; -import { useRef, useEffect, useState, CSSProperties } from 'react'; +import React, { useRef, useEffect, useState, useMemo, CSSProperties } from 'react'; +import { useDebouncedCallback } from 'use-debounce'; import { PATTERN_ASSEMBLER_EVENTS } from './events'; import PatternActionBar from './pattern-action-bar'; import { encodePatternId } from './utils'; @@ -28,6 +30,8 @@ interface Props { // The pattern renderer element has 1px min height before the pattern is loaded const PATTERN_RENDERER_MIN_HEIGHT = 1; +const LARGE_PREVIEW_OFFSET_TOP = 110; + const PatternLargePreview = ( { header, sections, @@ -43,47 +47,87 @@ const PatternLargePreview = ( { isNewSite, }: Props ) => { const translate = useTranslate(); - const hasSelectedPattern = header || sections.length || footer; + const hasSelectedPattern = Boolean( header || sections.length || footer ); const frameRef = useRef< HTMLDivElement | null >( null ); const listRef = useRef< HTMLUListElement | null >( null ); const [ viewportHeight, setViewportHeight ] = useState< number | undefined >( 0 ); const [ device, setDevice ] = useState< string >( 'computer' ); - const [ blockGap ] = useGlobalStyle( 'spacing.blockGap' ); + const [ zoomOutScale, setZoomOutScale ] = useState( 1 ); const [ backgroundColor ] = useGlobalStyle( 'color.background' ); - const [ patternLargePreviewStyle, setPatternLargePreviewStyle ] = useState( { - '--pattern-large-preview-background': backgroundColor, - } as CSSProperties ); + const patternLargePreviewStyle = useMemo( + () => + ( { + '--pattern-large-preview-zoom-out-scale': zoomOutScale, + '--pattern-large-preview-background': backgroundColor, + } ) as CSSProperties, + [ zoomOutScale, backgroundColor ] + ); + + const [ debouncedRecordZoomOutScaleChange ] = useDebouncedCallback( ( value: number ) => { + recordTracksEvent( PATTERN_ASSEMBLER_EVENTS.LARGE_PREVIEW_ZOOM_OUT_SCALE_CHANGE, { + zoom_out_scale: value, + } ); + }, 1000 ); + + const [ activeElement, setActiveElement ] = useState< HTMLElement | null >( null ); + + const popoverAnchor = useMemo( () => { + if ( ! activeElement ) { + return undefined; + } + + return { + getBoundingClientRect() { + const { left, top, width, height } = activeElement.getBoundingClientRect(); + + return new window.DOMRect( + left, + // Stick to the top when the partial area of the active element is out of the viewport + Math.max( top, LARGE_PREVIEW_OFFSET_TOP ), + width, + height + ); + }, + }; + }, [ activeElement ] ); const renderPattern = ( type: string, pattern: Pattern, position = -1 ) => { - const key = type === 'section' ? pattern.key : type; - const handleShuffle = () => onShuffle( type, pattern, position ); - const getActionBarProps = () => { + const isSection = type === 'section'; + const clientId = isSection ? pattern.key : type; + const isActive = activeElement?.dataset?.clientId === clientId; + + const handleMouseEnter = ( event: React.MouseEvent< HTMLElement > ) => { + setActiveElement( event.currentTarget ); + }; + + const handleMouseLeave = ( event: React.MouseEvent< HTMLElement > ) => { + if ( ! frameRef.current?.contains( event.relatedTarget as Node ) ) { + setActiveElement( null ); + } + }; + + const handleDelete = () => { + setActiveElement( null ); if ( type === 'header' ) { - return { onDelete: onDeleteHeader, onShuffle: handleShuffle }; + onDeleteHeader(); } else if ( type === 'footer' ) { - return { onDelete: onDeleteFooter, onShuffle: handleShuffle }; + onDeleteFooter(); + } else { + onDeleteSection( position ); } - - return { - disableMoveUp: position === 0, - disableMoveDown: sections?.length === position + 1, - onDelete: () => onDeleteSection( position ), - onMoveUp: () => onMoveUpSection( position ), - onMoveDown: () => onMoveDownSection( position ), - onShuffle: handleShuffle, - }; }; return (
  • - { viewportHeight && ( + { !! viewportHeight && ( ) } - + { isActive && ( + + onMoveUpSection( position ) : undefined } + onMoveDown={ isSection ? () => onMoveDownSection( position ) : undefined } + onShuffle={ () => onShuffle( type, pattern, position ) } + onDelete={ handleDelete } + onMouseLeave={ handleMouseLeave } + /> + + ) }
  • ); }; @@ -108,6 +171,13 @@ const PatternLargePreview = ( { setViewportHeight( height ); }; + const handleZoomOutScale = ( value: number ) => { + setZoomOutScale( value ); + if ( zoomOutScale !== value ) { + debouncedRecordZoomOutScaleChange( value ); + } + }; + // Scroll to newly added patterns useEffect( () => { let timerId: number; @@ -139,13 +209,20 @@ const PatternLargePreview = ( { }; }, [ activePosition, header, sections, footer ] ); - // Delay updating the styles to make the transition smooth - // See https://github.com/Automattic/wp-calypso/pull/74033#issuecomment-1453056703 + // Unset the hovered element when the mouse is leaving the large preview useEffect( () => { - setPatternLargePreviewStyle( { - '--pattern-large-preview-background': backgroundColor, - } as CSSProperties ); - }, [ blockGap, backgroundColor ] ); + const handleMouseLeave = ( event: MouseEvent ) => { + const relatedTarget = event.relatedTarget as HTMLElement | null; + if ( ! relatedTarget?.closest( '.pattern-assembler__pattern-action-bar' ) ) { + setActiveElement( null ); + } + }; + + frameRef.current?.addEventListener( 'mouseleave', handleMouseLeave ); + return () => { + frameRef.current?.removeEventListener( 'mouseleave', handleMouseLeave ); + }; + }, [ frameRef, setActiveElement ] ); return ( { recordTracksEvent( PATTERN_ASSEMBLER_EVENTS.PREVIEW_DEVICE_CLICK, { device } ); setDevice( device ); } } onViewportChange={ updateViewportHeight } + onZoomOutScaleChange={ handleZoomOutScale } > { hasSelectedPattern ? (
      void; - onReplaceHeader?: () => void; - onDeleteHeader?: () => void; - onAddSection: () => void; - onReplaceSection: ( position: number ) => void; - onDeleteSection: ( position: number ) => void; - onMoveUpSection: ( position: number ) => void; - onMoveDownSection: ( position: number ) => void; - onAddFooter?: () => void; - onReplaceFooter?: () => void; - onDeleteFooter?: () => void; - onShuffle: ( type: string, pattern: Pattern, position?: number ) => void; -}; - -const PatternLayout = ( { - sections, - onAddSection, - onReplaceSection, - onDeleteSection, - onMoveUpSection, - onMoveDownSection, - onShuffle, -}: PatternLayoutProps ) => { - const translate = useTranslate(); - - return ( -
      - { sections.length > 0 && ( - }> - { ( m: any ) => ( - - { sections.map( ( pattern: Pattern, index ) => { - const { title, category, key } = pattern; - return ( - - - { `${ index + 1 }. ${ category?.label }` } - - onReplaceSection( index ) } - onDelete={ () => onDeleteSection( index ) } - onMoveUp={ () => onMoveUpSection( index ) } - onMoveDown={ () => onMoveDownSection( index ) } - onShuffle={ () => onShuffle( 'sections', pattern, index ) } - disableMoveUp={ index === 0 } - disableMoveDown={ sections?.length === index + 1 } - /> - - ); - } ) } - - ) } - - ) } - -
      - ); -}; - -export default PatternLayout; diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/style.scss b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/style.scss index a1d9de3465d05..fa0d4b4b18b8d 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/style.scss +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/pattern-assembler/style.scss @@ -79,125 +79,6 @@ $font-family: "SF Pro Text", $sans; } } - /** - * Pattern Layout - */ - .pattern-layout { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - margin: 0; - - button { - display: flex; - align-items: center; - border: 0; - padding: 0; - font-family: inherit; - color: var(--color-link); - transition: color 0.2s ease-in; - background-color: transparent; - } - - .pattern-layout__list { - list-style: none; - font-family: Inter, sans-serif; - font-style: normal; - font-weight: 500; - font-size: $font-body-small; - color: #101517; - letter-spacing: -0.24px; - user-select: none; - overflow-y: auto; - overflow-x: hidden; - scrollbar-width: none; - // Fix for tooltip position issue - padding-bottom: 25px; - - &::-webkit-scrollbar { - display: none; - } - } - - .pattern-layout__list-item { - position: relative; - display: flex; - align-items: center; - list-style: none; - height: 48px; - line-height: 48px; - border-bottom: 1px solid #eee; - - .pattern-layout__list-item-text { - text-overflow: ellipsis; - overflow: hidden; - max-width: 235px; - white-space: nowrap; - } - - &:hover, - &:focus, - &:focus-within { - button { - color: var(--color-link-dark); - } - - .pattern-layout__list-item-text { - max-width: 154px; - } - - .pattern-action-bar { - animation: slideInShort 0.2s forwards, fadeIn 0.3s forwards; - animation-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95); - } - } - - &:last-child { - border-bottom: 0; - } - - &:first-child { - margin-top: auto; - } - } - - .pattern-layout__list-item-button { - width: 100%; - height: 100%; - } - - .pattern-layout__add-button { - flex-shrink: 0; - gap: 16px; - font-family: Inter, sans-serif; - - .pattern-layout__add-button-icon { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 5px 6px 6px; - border-radius: 2px; - box-sizing: border-box; - color: var(--studio-white); - background-color: var(--color-link); - transition: background-color 0.2s ease-in; - } - - &:hover, - &:focus, - &:focus-within { - color: var(--color-link-dark); - - .pattern-layout__add-button-icon { - background-color: var(--color-link-dark); - } - } - } - } - /** * Pattern Selector */ @@ -379,42 +260,3 @@ $font-family: "SF Pro Text", $sans; box-shadow: 0 0 0 2px var(--color-primary-light); } } - -.popover.is-right.pattern-layout__add-button-popover { - margin-left: 6px; - outline: none; - - .popover__arrow { - border-right-color: transparent; - - &::before { - border-width: 10px 16px; - border-right-color: var(--studio-black); - } - } - - .popover__inner { - line-height: 20px; - padding: 4px 8px; - border-radius: 4px; - border: 0; - color: #eee; - background-color: var(--studio-black); - } -} - -/** - * Revamp styles - */ -.pattern-assembler.pattern-assembler__sidebar-revamp { - .pattern-layout { - .pattern-layout__list { - // Fix for tooltip position issue - margin: 0 0 -9px; - } - - .pattern-action-bar { - right: 2px; - } - } -} diff --git a/packages/components/src/device-switcher/device-switcher.scss b/packages/components/src/device-switcher/device-switcher.scss index 5a5d28187dadb..95fd2ae5ad315 100644 --- a/packages/components/src/device-switcher/device-switcher.scss +++ b/packages/components/src/device-switcher/device-switcher.scss @@ -14,6 +14,77 @@ } } +.device-switcher__header { + display: flex; + align-items: center; + width: 100%; + padding: 0 10px; + box-sizing: border-box; + + .device-switcher__toolbar { + margin-bottom: 13px; + } + + .device-switcher__zoom-out { + --zoom-out-thumb-size: 8px; + --zoom-out-primary-color: var(--studio-gray-50, #50575e); + --zoom-out-secondary-color: var(--studio-gray-10, #c3c4c7); + --wp-components-color-accent: var(--zoom-out-primary-color); + + position: absolute; + right: 10px; + width: 15%; + max-width: 156px; + margin-bottom: 13px; + + &:hover { + --zoom-out-primary-color: var(--studio-gray-100, #101517); + } + + svg { + width: 21px; + height: 21px; + margin-top: -4px; + fill: var(--zoom-out-primary-color); + stroke: var(--zoom-out-primary-color); + } + + .components-range-control__wrapper:hover { + .components-range-control__thumb-wrapper { + transform: translate(4.5px, -50%) scale(1.5); + } + } + + .components-range-control__slider + span, + .components-range-control__track { + height: 3px; + background: var(--zoom-out-secondary-color); + } + + .components-range-control__track { + background: var(--zoom-out-primary-color); + } + + .components-range-control__thumb-wrapper { + width: var(--zoom-out-thumb-size); + height: var(--zoom-out-thumb-size); + top: 50%; + transform: translate(4.5px, -50%); + margin-top: 0; + border-color: var(--zoom-out-primary-color); + background: var(--zoom-out-primary-color); + transition: transform 0.15s ease-in-out; + + > span::before { + width: calc(var(--zoom-out-thumb-size) + 4px); + height: calc(var(--zoom-out-thumb-size) + 4px); + top: -2px; + left: -2px; + } + } + } +} + .device-switcher__container--frame-fixed-viewport { .device-switcher__viewport { position: relative; diff --git a/packages/components/src/device-switcher/device-switcher.tsx b/packages/components/src/device-switcher/device-switcher.tsx index ed96d12f7208b..6b3db4d502936 100644 --- a/packages/components/src/device-switcher/device-switcher.tsx +++ b/packages/components/src/device-switcher/device-switcher.tsx @@ -1,9 +1,12 @@ +import { RangeControl } from '@wordpress/components'; import { useResizeObserver } from '@wordpress/compose'; +import { search } from '@wordpress/icons'; import classnames from 'classnames'; import { useState, useEffect, useRef } from 'react'; import { DEVICE_TYPES } from './constants'; import FixedViewport, { useViewportScale } from './fixed-viewport'; import DeviceSwitcherToolbar from './toolbar'; +import useZoomOut from './use-zoom-out'; import type { Device } from './types'; import './device-switcher.scss'; @@ -16,9 +19,11 @@ interface Props { isShowFrameShadow?: boolean; isFixedViewport?: boolean; isFullscreen?: boolean; + isZoomable?: boolean; frameRef?: React.MutableRefObject< HTMLDivElement | null >; onDeviceChange?: ( device: Device ) => void; onViewportChange?: ( height?: number ) => void; + onZoomOutScaleChange?: ( value: number ) => void; } // Transition animation delay @@ -33,9 +38,11 @@ const DeviceSwitcher = ( { isShowFrameShadow = true, isFixedViewport, isFullscreen, + isZoomable, frameRef, onDeviceChange, onViewportChange, + onZoomOutScaleChange, }: Props ) => { const [ device, setDevice ] = useState< Device >( defaultDevice ); const [ containerResizeListener, { width, height } ] = useResizeObserver(); @@ -43,6 +50,8 @@ const DeviceSwitcher = ( { const viewportElement = frameRef?.current?.parentElement; const viewportWidth = viewportElement?.clientWidth as number; const viewportScale = useViewportScale( device, viewportWidth ); + const { zoomOutScale, zoomOutStyles, handleZoomOutScaleChange } = + useZoomOut( onZoomOutScaleChange ); const handleDeviceClick = ( nextDevice: Device ) => { setDevice( nextDevice ); @@ -60,18 +69,33 @@ const DeviceSwitcher = ( { // Trigger animation end after the duration timerRef.current = setTimeout( () => { timerRef.current = null; - onViewportChange?.( frameRef?.current?.clientHeight ); + const frameHeight = frameRef?.current?.getBoundingClientRect()?.height; + if ( frameHeight ) { + onViewportChange?.( frameHeight ); + } }, ANIMATION_DURATION ); return clearAnimationEndTimer; }, [ width, height, viewportScale, isFixedViewport ] ); - const frame = ( + let frame = (
      { children }
      ); + if ( isZoomable ) { + frame =
      { frame }
      ; + } + + if ( isFixedViewport ) { + frame = ( + + { frame } + + ); + } + return (
      - { isShowDeviceSwitcherToolbar && ( - - ) } - { isFixedViewport ? ( - - { frame } - - ) : ( - frame - ) } +
      + { isShowDeviceSwitcherToolbar && ( + + ) } + { isZoomable && ( + value !== undefined && handleZoomOutScaleChange( value / 100 ) } + min={ 25 } + max={ 100 } + /> + ) } +
      + { frame } { containerResizeListener }
      ); diff --git a/packages/components/src/device-switcher/toolbar.scss b/packages/components/src/device-switcher/toolbar.scss index 7d8cd9a99cafd..32da3ceada8b3 100644 --- a/packages/components/src/device-switcher/toolbar.scss +++ b/packages/components/src/device-switcher/toolbar.scss @@ -3,13 +3,13 @@ .device-switcher__toolbar { display: none; + flex: 1; .device-switcher__toolbar-devices { align-items: center; display: flex; height: 58px; gap: 18px; - margin-bottom: 13px; justify-content: center; > button { diff --git a/packages/components/src/device-switcher/use-zoom-out.ts b/packages/components/src/device-switcher/use-zoom-out.ts new file mode 100644 index 0000000000000..1f51dee134ae5 --- /dev/null +++ b/packages/components/src/device-switcher/use-zoom-out.ts @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react'; +import type { CSSProperties } from 'react'; + +const INITIAL_SCALE = 1; + +const useZoomOut = ( onZoomOutScaleChange?: ( value: number ) => void ) => { + const [ zoomOutScale, setZoomOutScale ] = useState( INITIAL_SCALE ); + + const zoomOutStyles = { + display: 'flex', + justifyContent: 'center', + width: '100%', + height: `calc( 100% / ${ zoomOutScale } )`, + transform: `scale( ${ zoomOutScale } )`, + transformOrigin: 'top', + } as CSSProperties; + + const handleZoomOutScaleChange = ( value: number ) => { + setZoomOutScale( value ); + onZoomOutScaleChange?.( value ); + }; + + useEffect( () => { + onZoomOutScaleChange?.( zoomOutScale ); + }, [] ); + + return { + zoomOutScale, + zoomOutStyles, + handleZoomOutScaleChange, + }; +}; + +export default useZoomOut;