@elementor/editor-variables 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/index.js +918 -332
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +961 -329
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +10 -9
  7. package/src/components/color-variable-creation.tsx +65 -64
  8. package/src/components/color-variable-edit.tsx +117 -0
  9. package/src/components/color-variables-selection.tsx +98 -52
  10. package/src/components/font-variable-creation.tsx +143 -0
  11. package/src/components/font-variable-edit.tsx +146 -0
  12. package/src/components/font-variables-selection.tsx +97 -51
  13. package/src/components/ui/menu-item-content.tsx +51 -0
  14. package/src/components/ui/no-search-results.tsx +38 -0
  15. package/src/components/ui/no-variables.tsx +35 -0
  16. package/src/components/ui/styled-menu-list.tsx +31 -0
  17. package/src/components/variable-selection-popover.tsx +133 -0
  18. package/src/components/variables-repeater-item-slot.tsx +29 -0
  19. package/src/controls/color-variable-control.tsx +90 -0
  20. package/src/controls/font-variable-control.tsx +88 -0
  21. package/src/create-style-variables-repository.ts +3 -2
  22. package/src/hooks/use-prop-color-variable-action.tsx +7 -2
  23. package/src/hooks/use-prop-font-variable-action.tsx +7 -2
  24. package/src/hooks/use-prop-variables.ts +31 -4
  25. package/src/init-color-variables.ts +51 -3
  26. package/src/init-font-variables.ts +2 -2
  27. package/src/service.ts +23 -3
  28. package/src/storage.ts +5 -1
  29. package/src/types.ts +12 -8
  30. package/src/components/styled-menu-item.tsx +0 -10
  31. package/src/components/variables-selection-popover.tsx +0 -106
  32. package/src/controls/color-variables-selection-control.tsx +0 -34
  33. package/src/controls/font-variables-selection-control.tsx +0 -29
  34. /package/src/components/{color-indicator.tsx → ui/color-indicator.tsx} +0 -0
@@ -1,70 +1,116 @@
1
1
  import * as React from 'react';
2
- import { Fragment } from 'react';
2
+ import { useState } from 'react';
3
3
  import { useBoundProp } from '@elementor/editor-controls';
4
- import { EditIcon, TextIcon } from '@elementor/icons';
5
- import { Box, Divider, ListItemIcon, ListItemText, MenuList } from '@elementor/ui';
4
+ import { PopoverHeader, PopoverMenuList, PopoverSearch, type VirtualizedItem } from '@elementor/editor-ui';
5
+ import { ColorFilterIcon, PlusIcon, SettingsIcon, TextIcon } from '@elementor/icons';
6
+ import { Divider, IconButton } from '@elementor/ui';
7
+ import { __ } from '@wordpress/i18n';
6
8
 
7
- import { usePropVariables } from '../hooks/use-prop-variables';
9
+ import { useFilteredVariables } from '../hooks/use-prop-variables';
8
10
  import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
9
- import { type VariableKey } from '../types';
10
- import { StyledMenuItem } from './styled-menu-item';
11
+ import { type ExtendedVirtualizedItem } from '../types';
12
+ import { MenuItemContent } from './ui/menu-item-content';
13
+ import { NoSearchResults } from './ui/no-search-results';
14
+ import { NoVariables } from './ui/no-variables';
15
+ import { VariablesStyledMenuList } from './ui/styled-menu-list';
16
+
17
+ const SIZE = 'tiny';
11
18
 
12
19
  type Props = {
13
- onSelect?: () => void;
20
+ closePopover: () => void;
21
+ onAdd?: () => void;
22
+ onEdit?: ( key: string ) => void;
23
+ onSettings?: () => void;
14
24
  };
15
25
 
16
- export const FontVariablesSelection = ( { onSelect }: Props ) => {
26
+ export const FontVariablesSelection = ( { closePopover, onAdd, onEdit, onSettings }: Props ) => {
17
27
  const { value: variable, setValue: setVariable } = useBoundProp( fontVariablePropTypeUtil );
28
+ const [ searchValue, setSearchValue ] = useState( '' );
18
29
 
19
- const variables = usePropVariables( fontVariablePropTypeUtil.key );
30
+ const {
31
+ list: variables,
32
+ hasMatches: hasSearchResults,
33
+ isSourceNotEmpty: hasVariables,
34
+ } = useFilteredVariables( searchValue, fontVariablePropTypeUtil.key );
20
35
 
21
- const handleSetVariable = ( key: VariableKey ) => {
36
+ const handleSetVariable = ( key: string ) => {
22
37
  setVariable( key );
38
+ closePopover();
39
+ };
40
+
41
+ const actions = [];
42
+
43
+ if ( onAdd ) {
44
+ actions.push(
45
+ <IconButton key="add" size={ SIZE } onClick={ onAdd }>
46
+ <PlusIcon fontSize={ SIZE } />
47
+ </IconButton>
48
+ );
49
+ }
23
50
 
24
- onSelect?.();
51
+ if ( onSettings ) {
52
+ actions.push(
53
+ <IconButton key="settings" size={ SIZE } onClick={ onSettings }>
54
+ <SettingsIcon fontSize={ SIZE } />
55
+ </IconButton>
56
+ );
57
+ }
58
+
59
+ const items: ExtendedVirtualizedItem[] = variables.map( ( { value, label, key } ) => ( {
60
+ type: 'item' as const,
61
+ value: key,
62
+ label,
63
+ icon: <TextIcon />,
64
+ secondaryText: value,
65
+ onEdit: () => onEdit?.( key ),
66
+ } ) );
67
+
68
+ const handleSearch = ( search: string ) => {
69
+ setSearchValue( search );
70
+ };
71
+
72
+ const handleClearSearch = () => {
73
+ setSearchValue( '' );
25
74
  };
26
75
 
27
76
  return (
28
- <Fragment>
77
+ <>
78
+ <PopoverHeader
79
+ title={ __( 'Variables', 'elementor' ) }
80
+ onClose={ closePopover }
81
+ icon={ <ColorFilterIcon fontSize={ SIZE } /> }
82
+ actions={ actions }
83
+ />
84
+
85
+ { hasVariables && (
86
+ <PopoverSearch
87
+ value={ searchValue }
88
+ onSearch={ handleSearch }
89
+ placeholder={ __( 'Search', 'elementor' ) }
90
+ />
91
+ ) }
92
+
29
93
  <Divider />
30
- <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
31
- <MenuList role="listbox" tabIndex={ 0 }>
32
- { variables.map( ( { value, label, key } ) => (
33
- <StyledMenuItem
34
- key={ key }
35
- onClick={ () => handleSetVariable( key ) }
36
- selected={ key === variable }
37
- >
38
- <ListItemIcon>
39
- <TextIcon />
40
- </ListItemIcon>
41
- <ListItemText
42
- primary={ label }
43
- secondary={ value }
44
- primaryTypographyProps={ {
45
- variant: 'body2',
46
- color: 'text.secondary',
47
- style: {
48
- lineHeight: 2,
49
- display: 'inline-block',
50
- overflow: 'hidden',
51
- textOverflow: 'ellipsis',
52
- whiteSpace: 'nowrap',
53
- maxWidth: '81px',
54
- },
55
- } }
56
- secondaryTypographyProps={ {
57
- variant: 'caption',
58
- color: 'text.tertiary',
59
- style: { marginTop: '1px', lineHeight: '1' },
60
- } }
61
- sx={ { display: 'flex', alignItems: 'center', gap: 1 } }
62
- />
63
- <EditIcon color="action" fontSize="inherit" sx={ { mx: 1, opacity: '0' } } />
64
- </StyledMenuItem>
65
- ) ) }
66
- </MenuList>
67
- </Box>
68
- </Fragment>
94
+
95
+ { hasVariables && hasSearchResults && (
96
+ <PopoverMenuList
97
+ items={ items }
98
+ onSelect={ handleSetVariable }
99
+ onClose={ () => {} }
100
+ selectedValue={ variable }
101
+ data-testid="font-variables-list"
102
+ menuListTemplate={ VariablesStyledMenuList }
103
+ menuItemContentTemplate={ ( item: VirtualizedItem< 'item', string > ) => (
104
+ <MenuItemContent item={ item } />
105
+ ) }
106
+ />
107
+ ) }
108
+
109
+ { ! hasSearchResults && hasVariables && (
110
+ <NoSearchResults searchValue={ searchValue } onClear={ handleClearSearch } />
111
+ ) }
112
+
113
+ { ! hasVariables && <NoVariables icon={ <TextIcon fontSize="large" /> } onAdd={ onAdd } /> }
114
+ </>
69
115
  );
70
116
  };
@@ -0,0 +1,51 @@
1
+ import * as React from 'react';
2
+ import type { VirtualizedItem } from '@elementor/editor-ui';
3
+ import { EditIcon } from '@elementor/icons';
4
+ import { IconButton, ListItemIcon, ListItemText } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ const SIZE = 'tiny';
8
+
9
+ export const MenuItemContent = < T, V extends string >( { item }: { item: VirtualizedItem< T, V > } ) => {
10
+ const onEdit = item.onEdit as ( ( value: V ) => void ) | undefined;
11
+
12
+ return (
13
+ <>
14
+ <ListItemIcon>{ item.icon }</ListItemIcon>
15
+ <ListItemText
16
+ primary={ item.label || item.value }
17
+ secondary={ item.secondaryText }
18
+ primaryTypographyProps={ {
19
+ variant: 'body2',
20
+ color: 'text.secondary',
21
+ style: {
22
+ lineHeight: 2,
23
+ display: 'inline-block',
24
+ overflow: 'hidden',
25
+ textOverflow: 'ellipsis',
26
+ whiteSpace: 'nowrap',
27
+ maxWidth: '81px',
28
+ },
29
+ } }
30
+ secondaryTypographyProps={ {
31
+ variant: 'caption',
32
+ color: 'text.tertiary',
33
+ style: { marginTop: '1px', lineHeight: '1' },
34
+ } }
35
+ sx={ { display: 'flex', alignItems: 'center', gap: 1 } }
36
+ />
37
+ { !! onEdit && (
38
+ <IconButton
39
+ sx={ { mx: 1, opacity: '0' } }
40
+ onClick={ ( e: React.MouseEvent< HTMLButtonElement > ) => {
41
+ e.stopPropagation();
42
+ onEdit( item.value );
43
+ } }
44
+ aria-label={ __( 'Edit', 'elementor' ) }
45
+ >
46
+ <EditIcon color="action" fontSize={ SIZE } />
47
+ </IconButton>
48
+ ) }
49
+ </>
50
+ );
51
+ };
@@ -0,0 +1,38 @@
1
+ import * as React from 'react';
2
+ import { ColorFilterIcon } from '@elementor/icons';
3
+ import { Link, Stack, Typography } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ type Props = {
7
+ searchValue: string;
8
+ onClear?: () => void;
9
+ };
10
+
11
+ export const NoSearchResults = ( { searchValue, onClear }: Props ) => {
12
+ return (
13
+ <Stack
14
+ gap={ 1 }
15
+ alignItems="center"
16
+ justifyContent="center"
17
+ height="100%"
18
+ color="text.secondary"
19
+ sx={ { p: 2.5, pb: 5.5 } }
20
+ >
21
+ <ColorFilterIcon fontSize="large" />
22
+
23
+ <Typography align="center" variant="subtitle2">
24
+ { __( 'Sorry, nothing matched', 'elementor' ) }
25
+ <br />
26
+ &ldquo;{ searchValue }&rdquo;.
27
+ </Typography>
28
+
29
+ <Typography align="center" variant="caption">
30
+ { __( 'Try something else.', 'elementor' ) }
31
+ <br />
32
+ <Link color="text.secondary" variant="caption" component="button" onClick={ onClear }>
33
+ { __( 'Clear & try again', 'elementor' ) }
34
+ </Link>
35
+ </Typography>
36
+ </Stack>
37
+ );
38
+ };
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { Button, Stack, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ type Props = {
6
+ icon?: React.ReactNode;
7
+ onAdd?: () => void;
8
+ };
9
+
10
+ export const NoVariables = ( { icon, onAdd }: Props ) => (
11
+ <Stack
12
+ gap={ 1 }
13
+ alignItems="center"
14
+ justifyContent="center"
15
+ height="100%"
16
+ color="text.secondary"
17
+ sx={ { p: 2.5, pb: 5.5 } }
18
+ >
19
+ { icon }
20
+
21
+ <Typography align="center" variant="subtitle2">
22
+ { __( 'Create your first variable', 'elementor' ) }
23
+ </Typography>
24
+
25
+ <Typography align="center" variant="caption" maxWidth="180px">
26
+ { __( 'Variables are saved attributes that you can apply anywhere on your site.', 'elementor' ) }
27
+ </Typography>
28
+
29
+ { onAdd && (
30
+ <Button variant="outlined" color="secondary" size="small" onClick={ onAdd }>
31
+ { __( 'Create a variable', 'elementor' ) }
32
+ </Button>
33
+ ) }
34
+ </Stack>
35
+ );
@@ -0,0 +1,31 @@
1
+ import { MenuList, styled } from '@elementor/ui';
2
+
3
+ export const VariablesStyledMenuList = styled( MenuList )( ( { theme } ) => ( {
4
+ '& > li': {
5
+ height: 32,
6
+ width: '100%',
7
+ display: 'flex',
8
+ alignItems: 'center',
9
+ },
10
+ '& > [role="option"]': {
11
+ ...theme.typography.caption,
12
+ lineHeight: 'inherit',
13
+ padding: theme.spacing( 0.5, 1, 0.5, 2 ),
14
+ '&:hover, &:focus': {
15
+ backgroundColor: theme.palette.action.hover,
16
+ },
17
+ '&[aria-selected="true"]': {
18
+ backgroundColor: theme.palette.action.selected,
19
+ },
20
+ cursor: 'pointer',
21
+ textOverflow: 'ellipsis',
22
+ position: 'absolute',
23
+ top: 0,
24
+ left: 0,
25
+ '&:hover .MuiIconButton-root, .MuiIconButton-root:focus': {
26
+ opacity: 1,
27
+ },
28
+ },
29
+ width: '100%',
30
+ position: 'relative',
31
+ } ) );
@@ -0,0 +1,133 @@
1
+ import * as React from 'react';
2
+ import { useRef, useState } from 'react';
3
+
4
+ import { colorVariablePropTypeUtil } from '../prop-types/color-variable-prop-type';
5
+ import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
6
+ import { type Variable } from '../types';
7
+ import { ColorVariableCreation } from './color-variable-creation';
8
+ import { ColorVariableEdit } from './color-variable-edit';
9
+ import { ColorVariablesSelection } from './color-variables-selection';
10
+ import { FontVariableCreation } from './font-variable-creation';
11
+ import { FontVariableEdit } from './font-variable-edit';
12
+ import { FontVariablesSelection } from './font-variables-selection';
13
+
14
+ const VIEW_LIST = 'list';
15
+ const VIEW_ADD = 'add';
16
+ const VIEW_EDIT = 'edit';
17
+
18
+ type View = typeof VIEW_LIST | typeof VIEW_ADD | typeof VIEW_EDIT;
19
+
20
+ type Props = {
21
+ closePopover: () => void;
22
+ propTypeKey: string;
23
+ selectedVariable?: Variable;
24
+ };
25
+
26
+ export const VariableSelectionPopover = ( { closePopover, propTypeKey, selectedVariable }: Props ) => {
27
+ const [ currentView, setCurrentView ] = useState< View >( VIEW_LIST );
28
+ const editIdRef = useRef< string >( '' );
29
+
30
+ return renderStage( {
31
+ propTypeKey,
32
+ currentView,
33
+ selectedVariable,
34
+ editIdRef,
35
+ setCurrentView,
36
+ closePopover,
37
+ } );
38
+ };
39
+
40
+ type StageProps = {
41
+ propTypeKey: string;
42
+ currentView: View;
43
+ selectedVariable?: Variable;
44
+ editIdRef: React.MutableRefObject< string >;
45
+ setCurrentView: ( stage: View ) => void;
46
+ closePopover: () => void;
47
+ };
48
+
49
+ function renderStage( props: StageProps ): React.ReactNode {
50
+ const handleSubmitOnEdit = () => {
51
+ if ( props?.selectedVariable?.key === props.editIdRef.current ) {
52
+ props.closePopover();
53
+ } else {
54
+ props.setCurrentView( VIEW_LIST );
55
+ }
56
+ };
57
+
58
+ if ( fontVariablePropTypeUtil.key === props.propTypeKey ) {
59
+ if ( VIEW_LIST === props.currentView ) {
60
+ return (
61
+ <FontVariablesSelection
62
+ closePopover={ props.closePopover }
63
+ onAdd={ () => {
64
+ props.setCurrentView( VIEW_ADD );
65
+ } }
66
+ onEdit={ ( key ) => {
67
+ props.editIdRef.current = key;
68
+ props.setCurrentView( VIEW_EDIT );
69
+ } }
70
+ />
71
+ );
72
+ }
73
+
74
+ if ( VIEW_ADD === props.currentView ) {
75
+ return (
76
+ <FontVariableCreation
77
+ onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
78
+ onClose={ props.closePopover }
79
+ />
80
+ );
81
+ }
82
+
83
+ if ( VIEW_EDIT === props.currentView ) {
84
+ return (
85
+ <FontVariableEdit
86
+ editId={ props.editIdRef.current ?? '' }
87
+ onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
88
+ onClose={ props.closePopover }
89
+ onSubmit={ handleSubmitOnEdit }
90
+ />
91
+ );
92
+ }
93
+ }
94
+
95
+ if ( colorVariablePropTypeUtil.key === props.propTypeKey ) {
96
+ if ( VIEW_LIST === props.currentView ) {
97
+ return (
98
+ <ColorVariablesSelection
99
+ closePopover={ props.closePopover }
100
+ onAdd={ () => {
101
+ props.setCurrentView( VIEW_ADD );
102
+ } }
103
+ onEdit={ ( key ) => {
104
+ props.editIdRef.current = key;
105
+ props.setCurrentView( VIEW_EDIT );
106
+ } }
107
+ />
108
+ );
109
+ }
110
+
111
+ if ( VIEW_ADD === props.currentView ) {
112
+ return (
113
+ <ColorVariableCreation
114
+ onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
115
+ onClose={ props.closePopover }
116
+ />
117
+ );
118
+ }
119
+
120
+ if ( VIEW_EDIT === props.currentView ) {
121
+ return (
122
+ <ColorVariableEdit
123
+ editId={ props.editIdRef.current ?? '' }
124
+ onGoBack={ () => props.setCurrentView( VIEW_LIST ) }
125
+ onClose={ props.closePopover }
126
+ onSubmit={ handleSubmitOnEdit }
127
+ />
128
+ );
129
+ }
130
+ }
131
+
132
+ return null;
133
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ import { type BackgroundColorOverlayPropValue, type BoxShadowPropValue, type PropValue } from '@elementor/editor-props';
3
+
4
+ import { useVariable } from '../hooks/use-prop-variables';
5
+ import { ColorIndicator } from './ui/color-indicator';
6
+
7
+ const useColorVariable = ( value: BackgroundColorOverlayPropValue | BoxShadowPropValue ) => {
8
+ const variableId = value?.value?.color?.value;
9
+
10
+ return useVariable( variableId || '' );
11
+ };
12
+
13
+ export const BackgroundRepeaterColorIndicator = ( { value }: { value: PropValue } ) => {
14
+ const colorVariable = useColorVariable( value as BackgroundColorOverlayPropValue );
15
+
16
+ return <ColorIndicator component="span" size="inherit" value={ colorVariable?.value } />;
17
+ };
18
+
19
+ export const BackgroundRepeaterLabel = ( { value }: { value: PropValue } ) => {
20
+ const colorVariable = useColorVariable( value as BackgroundColorOverlayPropValue );
21
+
22
+ return <span>{ colorVariable?.label }</span>;
23
+ };
24
+
25
+ export const BoxShadowRepeaterColorIndicator = ( { value }: { value: PropValue } ) => {
26
+ const colorVariable = useColorVariable( value as BoxShadowPropValue );
27
+
28
+ return <ColorIndicator component="span" size="inherit" value={ colorVariable?.value } />;
29
+ };
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+ import { useId, useRef } from 'react';
3
+ import { useBoundProp } from '@elementor/editor-controls';
4
+ import { colorPropTypeUtil } from '@elementor/editor-props';
5
+ import { ColorFilterIcon, DetachIcon } from '@elementor/icons';
6
+ import {
7
+ bindPopover,
8
+ bindTrigger,
9
+ Box,
10
+ IconButton,
11
+ Popover,
12
+ Stack,
13
+ Typography,
14
+ UnstableTag as Tag,
15
+ usePopupState,
16
+ } from '@elementor/ui';
17
+ import { __ } from '@wordpress/i18n';
18
+
19
+ import { ColorIndicator } from '../components/ui/color-indicator';
20
+ import { VariableSelectionPopover } from '../components/variable-selection-popover';
21
+ import { useVariable } from '../hooks/use-prop-variables';
22
+ import { colorVariablePropTypeUtil } from '../prop-types/color-variable-prop-type';
23
+
24
+ const SIZE = 'tiny';
25
+
26
+ export const ColorVariableControl = () => {
27
+ const { setValue: setColor } = useBoundProp();
28
+ const { value: variableValue } = useBoundProp( colorVariablePropTypeUtil );
29
+
30
+ const anchorRef = useRef< HTMLDivElement >( null );
31
+
32
+ const popupId = useId();
33
+ const popupState = usePopupState( {
34
+ variant: 'popover',
35
+ popupId: `elementor-variables-list-${ popupId }`,
36
+ } );
37
+
38
+ const selectedVariable = useVariable( variableValue );
39
+ if ( ! selectedVariable ) {
40
+ throw new Error( `Global color variable ${ variableValue } not found` );
41
+ }
42
+
43
+ const unlinkVariable = () => {
44
+ setColor( colorPropTypeUtil.create( selectedVariable.value ) );
45
+ };
46
+
47
+ return (
48
+ <Box ref={ anchorRef }>
49
+ <Tag
50
+ fullWidth
51
+ showActionsOnHover
52
+ startIcon={
53
+ <Stack spacing={ 0.75 } direction="row" alignItems="center">
54
+ <ColorIndicator size="inherit" value={ selectedVariable.value } component="span" />
55
+ <ColorFilterIcon fontSize="inherit" sx={ { mr: 1 } } />
56
+ </Stack>
57
+ }
58
+ label={
59
+ <Box sx={ { display: 'inline-grid', minWidth: 0 } }>
60
+ <Typography
61
+ sx={ { textOverflow: 'ellipsis', overflowX: 'hidden', lineHeight: 1 } }
62
+ variant="caption"
63
+ >
64
+ { selectedVariable.label }
65
+ </Typography>
66
+ </Box>
67
+ }
68
+ actions={
69
+ <IconButton size={ SIZE } onClick={ unlinkVariable } aria-label={ __( 'Unlink', 'elementor' ) }>
70
+ <DetachIcon fontSize={ SIZE } />
71
+ </IconButton>
72
+ }
73
+ { ...bindTrigger( popupState ) }
74
+ />
75
+ <Popover
76
+ disableScrollLock
77
+ anchorEl={ anchorRef.current }
78
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
79
+ transformOrigin={ { vertical: 'top', horizontal: 'right' } }
80
+ { ...bindPopover( popupState ) }
81
+ >
82
+ <VariableSelectionPopover
83
+ selectedVariable={ selectedVariable }
84
+ closePopover={ popupState.close }
85
+ propTypeKey={ colorVariablePropTypeUtil.key }
86
+ />
87
+ </Popover>
88
+ </Box>
89
+ );
90
+ };
@@ -0,0 +1,88 @@
1
+ import * as React from 'react';
2
+ import { useId, useRef } from 'react';
3
+ import { useBoundProp } from '@elementor/editor-controls';
4
+ import { stringPropTypeUtil } from '@elementor/editor-props';
5
+ import { ColorFilterIcon, DetachIcon } from '@elementor/icons';
6
+ import {
7
+ bindPopover,
8
+ bindTrigger,
9
+ Box,
10
+ IconButton,
11
+ Popover,
12
+ Stack,
13
+ Typography,
14
+ UnstableTag as Tag,
15
+ usePopupState,
16
+ } from '@elementor/ui';
17
+ import { __ } from '@wordpress/i18n';
18
+
19
+ import { VariableSelectionPopover } from '../components/variable-selection-popover';
20
+ import { useVariable } from '../hooks/use-prop-variables';
21
+ import { fontVariablePropTypeUtil } from '../prop-types/font-variable-prop-type';
22
+
23
+ const SIZE = 'tiny';
24
+
25
+ export const FontVariableControl = () => {
26
+ const { setValue: setFontFamily } = useBoundProp();
27
+ const { value: variableValue } = useBoundProp( fontVariablePropTypeUtil );
28
+
29
+ const anchorRef = useRef< HTMLDivElement >( null );
30
+
31
+ const popupId = useId();
32
+ const popupState = usePopupState( {
33
+ variant: 'popover',
34
+ popupId: `elementor-variables-list-${ popupId }`,
35
+ } );
36
+
37
+ const selectedVariable = useVariable( variableValue );
38
+ if ( ! selectedVariable ) {
39
+ throw new Error( `Global font variable ${ variableValue } not found` );
40
+ }
41
+
42
+ const unlinkVariable = () => {
43
+ setFontFamily( stringPropTypeUtil.create( selectedVariable.value ) );
44
+ };
45
+
46
+ return (
47
+ <Box ref={ anchorRef }>
48
+ <Tag
49
+ fullWidth
50
+ showActionsOnHover
51
+ startIcon={
52
+ <Stack spacing={ 0.75 } direction="row" alignItems="center">
53
+ <ColorFilterIcon fontSize={ 'inherit' } sx={ { mr: 1 } } />
54
+ </Stack>
55
+ }
56
+ label={
57
+ <Box sx={ { display: 'inline-grid', minWidth: 0 } }>
58
+ <Typography
59
+ sx={ { textOverflow: 'ellipsis', overflowX: 'hidden', lineHeight: 1 } }
60
+ variant="caption"
61
+ >
62
+ { selectedVariable.label }
63
+ </Typography>
64
+ </Box>
65
+ }
66
+ actions={
67
+ <IconButton size={ SIZE } onClick={ unlinkVariable } aria-label={ __( 'Unlink', 'elementor' ) }>
68
+ <DetachIcon fontSize={ SIZE } />
69
+ </IconButton>
70
+ }
71
+ { ...bindTrigger( popupState ) }
72
+ />
73
+ <Popover
74
+ disableScrollLock
75
+ anchorEl={ anchorRef.current }
76
+ anchorOrigin={ { vertical: 'bottom', horizontal: 'right' } }
77
+ transformOrigin={ { vertical: 'top', horizontal: 'right' } }
78
+ { ...bindPopover( popupState ) }
79
+ >
80
+ <VariableSelectionPopover
81
+ selectedVariable={ selectedVariable }
82
+ closePopover={ popupState.close }
83
+ propTypeKey={ fontVariablePropTypeUtil.key }
84
+ />
85
+ </Popover>
86
+ </Box>
87
+ );
88
+ };
@@ -1,6 +1,7 @@
1
- import { type StyleVariables, type Variables, type VariableValue } from './types';
1
+ import { type StyleVariables, type Variable } from './types';
2
2
 
3
3
  type VariablesChangeCallback = ( variables: StyleVariables ) => void;
4
+ type Variables = Record< string, Variable >;
4
5
 
5
6
  export const createStyleVariablesRepository = () => {
6
7
  const variables: StyleVariables = {};
@@ -20,7 +21,7 @@ export const createStyleVariablesRepository = () => {
20
21
  }
21
22
  };
22
23
 
23
- const shouldUpdate = ( key: string, newValue: VariableValue ): boolean => {
24
+ const shouldUpdate = ( key: string, newValue: string ): boolean => {
24
25
  return ! ( key in variables ) || variables[ key ] !== newValue;
25
26
  };
26
27