fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,372 @@
/**
* External dependencies
*/
import { startOfMinute, format, set, setHours, setMonth } from 'date-fns';
/**
* WordPress dependencies
*/
import { useState, useMemo, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import BaseControl from '../../base-control';
import Button from '../../button';
import ButtonGroup from '../../button-group';
import SelectControl from '../../select-control';
import TimeZone from './timezone';
import type { TimePickerProps } from '../types';
import {
Wrapper,
Fieldset,
HoursInput,
TimeSeparator,
MinutesInput,
MonthSelectWrapper,
DayInput,
YearInput,
TimeWrapper,
} from './styles';
import { HStack } from '../../h-stack';
import { Spacer } from '../../spacer';
import type { InputChangeCallback } from '../../input-control/types';
import type { InputState } from '../../input-control/reducer/state';
import type { InputAction } from '../../input-control/reducer/actions';
import {
COMMIT,
PRESS_DOWN,
PRESS_UP,
} from '../../input-control/reducer/actions';
import { inputToDate } from '../utils';
import { TIMEZONELESS_FORMAT } from '../constants';
function from12hTo24h( hours: number, isPm: boolean ) {
return isPm ? ( ( hours % 12 ) + 12 ) % 24 : hours % 12;
}
/**
* Creates an InputControl reducer used to pad an input so that it is always a
* given width. For example, the hours and minutes inputs are padded to 2 so
* that '4' appears as '04'.
*
* @param pad How many digits the value should be.
*/
function buildPadInputStateReducer( pad: number ) {
return ( state: InputState, action: InputAction ) => {
const nextState = { ...state };
if (
action.type === COMMIT ||
action.type === PRESS_UP ||
action.type === PRESS_DOWN
) {
if ( nextState.value !== undefined ) {
nextState.value = nextState.value
.toString()
.padStart( pad, '0' );
}
}
return nextState;
};
}
/**
* TimePicker is a React component that renders a clock for time selection.
*
* ```jsx
* import { TimePicker } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const MyTimePicker = () => {
* const [ time, setTime ] = useState( new Date() );
*
* return (
* <TimePicker
* currentTime={ date }
* onChange={ ( newTime ) => setTime( newTime ) }
* is12Hour
* />
* );
* };
* ```
*/
export function TimePicker( {
is12Hour,
currentTime,
onChange,
}: TimePickerProps ) {
const [ date, setDate ] = useState( () =>
// Truncate the date at the minutes, see: #15495.
currentTime ? startOfMinute( inputToDate( currentTime ) ) : new Date()
);
// Reset the state when currentTime changed.
// TODO: useEffect() shouldn't be used like this, causes an unnecessary render
useEffect( () => {
setDate(
currentTime
? startOfMinute( inputToDate( currentTime ) )
: new Date()
);
}, [ currentTime ] );
const { day, month, year, minutes, hours, am } = useMemo(
() => ( {
day: format( date, 'dd' ),
month: format( date, 'MM' ),
year: format( date, 'yyyy' ),
minutes: format( date, 'mm' ),
hours: format( date, is12Hour ? 'hh' : 'HH' ),
am: format( date, 'a' ),
} ),
[ date, is12Hour ]
);
const buildNumberControlChangeCallback = (
method: 'hours' | 'minutes' | 'date' | 'year'
) => {
const callback: InputChangeCallback = ( value, { event } ) => {
// `instanceof` checks need to get the instance definition from the
// corresponding window object — therefore, the following logic makes
// the component work correctly even when rendered inside an iframe.
const HTMLInputElementInstance =
( event.target as HTMLInputElement )?.ownerDocument.defaultView
?.HTMLInputElement ?? HTMLInputElement;
if ( ! ( event.target instanceof HTMLInputElementInstance ) ) {
return;
}
if ( ! event.target.validity.valid ) {
return;
}
// We can safely assume value is a number if target is valid.
let numberValue = Number( value );
// If the 12-hour format is being used and the 'PM' period is
// selected, then the incoming value (which ranges 1-12) should be
// increased by 12 to match the expected 24-hour format.
if ( method === 'hours' && is12Hour ) {
numberValue = from12hTo24h( numberValue, am === 'PM' );
}
const newDate = set( date, { [ method ]: numberValue } );
setDate( newDate );
onChange?.( format( newDate, TIMEZONELESS_FORMAT ) );
};
return callback;
};
function buildAmPmChangeCallback( value: 'AM' | 'PM' ) {
return () => {
if ( am === value ) {
return;
}
const parsedHours = parseInt( hours, 10 );
const newDate = setHours(
date,
from12hTo24h( parsedHours, value === 'PM' )
);
setDate( newDate );
onChange?.( format( newDate, TIMEZONELESS_FORMAT ) );
};
}
const dayField = (
<DayInput
className="components-datetime__time-field components-datetime__time-field-day" // Unused, for backwards compatibility.
label={ __( 'Day' ) }
hideLabelFromVision
__next40pxDefaultSize
value={ day }
step={ 1 }
min={ 1 }
max={ 31 }
required
spinControls="none"
isPressEnterToChange
isDragEnabled={ false }
isShiftStepEnabled={ false }
onChange={ buildNumberControlChangeCallback( 'date' ) }
/>
);
const monthField = (
<MonthSelectWrapper>
<SelectControl
className="components-datetime__time-field components-datetime__time-field-month" // Unused, for backwards compatibility.
label={ __( 'Month' ) }
hideLabelFromVision
__next40pxDefaultSize
__nextHasNoMarginBottom
value={ month }
options={ [
{ value: '01', label: __( 'January' ) },
{ value: '02', label: __( 'February' ) },
{ value: '03', label: __( 'March' ) },
{ value: '04', label: __( 'April' ) },
{ value: '05', label: __( 'May' ) },
{ value: '06', label: __( 'June' ) },
{ value: '07', label: __( 'July' ) },
{ value: '08', label: __( 'August' ) },
{ value: '09', label: __( 'September' ) },
{ value: '10', label: __( 'October' ) },
{ value: '11', label: __( 'November' ) },
{ value: '12', label: __( 'December' ) },
] }
onChange={ ( value ) => {
const newDate = setMonth( date, Number( value ) - 1 );
setDate( newDate );
onChange?.( format( newDate, TIMEZONELESS_FORMAT ) );
} }
/>
</MonthSelectWrapper>
);
return (
<Wrapper
className="components-datetime__time" // Unused, for backwards compatibility.
>
<Fieldset>
<BaseControl.VisualLabel
as="legend"
className="components-datetime__time-legend" // Unused, for backwards compatibility.
>
{ __( 'Time' ) }
</BaseControl.VisualLabel>
<HStack
className="components-datetime__time-wrapper" // Unused, for backwards compatibility.
>
<TimeWrapper
className="components-datetime__time-field components-datetime__time-field-time" // Unused, for backwards compatibility.
>
<HoursInput
className="components-datetime__time-field-hours-input" // Unused, for backwards compatibility.
label={ __( 'Hours' ) }
hideLabelFromVision
__next40pxDefaultSize
value={ hours }
step={ 1 }
min={ is12Hour ? 1 : 0 }
max={ is12Hour ? 12 : 23 }
required
spinControls="none"
isPressEnterToChange
isDragEnabled={ false }
isShiftStepEnabled={ false }
onChange={ buildNumberControlChangeCallback(
'hours'
) }
__unstableStateReducer={ buildPadInputStateReducer(
2
) }
/>
<TimeSeparator
className="components-datetime__time-separator" // Unused, for backwards compatibility.
aria-hidden="true"
>
:
</TimeSeparator>
<MinutesInput
className="components-datetime__time-field-minutes-input" // Unused, for backwards compatibility.
label={ __( 'Minutes' ) }
hideLabelFromVision
__next40pxDefaultSize
value={ minutes }
step={ 1 }
min={ 0 }
max={ 59 }
required
spinControls="none"
isPressEnterToChange
isDragEnabled={ false }
isShiftStepEnabled={ false }
onChange={ buildNumberControlChangeCallback(
'minutes'
) }
__unstableStateReducer={ buildPadInputStateReducer(
2
) }
/>
</TimeWrapper>
{ is12Hour && (
<ButtonGroup
className="components-datetime__time-field components-datetime__time-field-am-pm" // Unused, for backwards compatibility.
>
<Button
className="components-datetime__time-am-button" // Unused, for backwards compatibility.
variant={
am === 'AM' ? 'primary' : 'secondary'
}
__next40pxDefaultSize
onClick={ buildAmPmChangeCallback( 'AM' ) }
>
{ __( 'AM' ) }
</Button>
<Button
className="components-datetime__time-pm-button" // Unused, for backwards compatibility.
variant={
am === 'PM' ? 'primary' : 'secondary'
}
__next40pxDefaultSize
onClick={ buildAmPmChangeCallback( 'PM' ) }
>
{ __( 'PM' ) }
</Button>
</ButtonGroup>
) }
<Spacer />
<TimeZone />
</HStack>
</Fieldset>
<Fieldset>
<BaseControl.VisualLabel
as="legend"
className="components-datetime__time-legend" // Unused, for backwards compatibility.
>
{ __( 'Date' ) }
</BaseControl.VisualLabel>
<HStack
className="components-datetime__time-wrapper" // Unused, for backwards compatibility.
>
{ is12Hour ? (
<>
{ monthField }
{ dayField }
</>
) : (
<>
{ dayField }
{ monthField }
</>
) }
<YearInput
className="components-datetime__time-field components-datetime__time-field-year" // Unused, for backwards compatibility.
label={ __( 'Year' ) }
hideLabelFromVision
__next40pxDefaultSize
value={ year }
step={ 1 }
min={ 1 }
max={ 9999 }
required
spinControls="none"
isPressEnterToChange
isDragEnabled={ false }
isShiftStepEnabled={ false }
onChange={ buildNumberControlChangeCallback( 'year' ) }
__unstableStateReducer={ buildPadInputStateReducer(
4
) }
/>
</HStack>
</Fieldset>
</Wrapper>
);
}
export default TimePicker;

View File

@@ -0,0 +1,107 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';
import { css } from '@emotion/react';
/**
* Internal dependencies
*/
import { COLORS, CONFIG } from '../../utils';
import { space } from '../../utils/space';
import {
Input,
BackdropUI,
} from '../../input-control/styles/input-control-styles';
import NumberControl from '../../number-control';
export const Wrapper = styled.div`
box-sizing: border-box;
font-size: ${ CONFIG.fontSize };
`;
export const Fieldset = styled.fieldset`
border: 0;
margin: 0 0 ${ space( 2 * 2 ) } 0;
padding: 0;
&:last-child {
margin-bottom: 0;
}
`;
export const TimeWrapper = styled.div`
direction: ltr;
display: flex;
`;
const baseInput = css`
&&& ${ Input } {
padding-left: ${ space( 2 ) };
padding-right: ${ space( 2 ) };
text-align: center;
}
`;
export const HoursInput = styled( NumberControl )`
${ baseInput }
width: ${ space( 9 ) };
&&& ${ Input } {
padding-right: 0;
}
&&& ${ BackdropUI } {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
`;
export const TimeSeparator = styled.span`
border-top: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 700 ] };
border-bottom: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 700 ] };
line-height: calc(
${ CONFIG.controlHeight } - ${ CONFIG.borderWidth } * 2
);
display: inline-block;
`;
export const MinutesInput = styled( NumberControl )`
${ baseInput }
width: ${ space( 9 ) };
&&& ${ Input } {
padding-left: 0;
}
&&& ${ BackdropUI } {
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
`;
// Ideally we wouldn't need a wrapper, but can't otherwise target the
// <BaseControl> in <SelectControl>
export const MonthSelectWrapper = styled.div`
flex-grow: 1;
`;
export const DayInput = styled( NumberControl )`
${ baseInput }
width: ${ space( 9 ) };
`;
export const YearInput = styled( NumberControl )`
${ baseInput }
width: ${ space( 14 ) };
`;
export const TimeZone = styled.div`
text-decoration: underline dotted;
`;

View File

@@ -0,0 +1,368 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import TimePicker from '..';
describe( 'TimePicker', () => {
it( 'should call onChange with updated date values', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const monthInput = screen.getByLabelText( 'Month' );
const dayInput = screen.getByLabelText( 'Day' );
const yearInput = screen.getByLabelText( 'Year' );
const hoursInput = screen.getByLabelText( 'Hours' );
const minutesInput = screen.getByLabelText( 'Minutes' );
await user.selectOptions( monthInput, '12' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-12-18T11:00:00' );
onChangeSpy.mockClear();
await user.clear( dayInput );
await user.type( dayInput, '22' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-12-22T11:00:00' );
onChangeSpy.mockClear();
await user.clear( yearInput );
await user.type( yearInput, '2018' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '2018-12-22T11:00:00' );
onChangeSpy.mockClear();
await user.clear( hoursInput );
await user.type( hoursInput, '12' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '2018-12-22T00:00:00' );
onChangeSpy.mockClear();
await user.clear( minutesInput );
await user.type( minutesInput, '35' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '2018-12-22T00:35:00' );
onChangeSpy.mockClear();
} );
it( 'should call onChange with an updated hour (12-hour clock)', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const hoursInput = screen.getByLabelText( 'Hours' );
await user.clear( hoursInput );
await user.type( hoursInput, '10' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T10:00:00' );
} );
it( 'should call onChange with a bounded hour (12-hour clock) if the hour is out of bounds', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const hoursInput = screen.getByLabelText( 'Hours' );
await user.clear( hoursInput );
await user.type( hoursInput, '22' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T00:00:00' );
} );
it( 'should call onChange with an updated hour (24-hour clock)', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour={ false }
/>
);
const hoursInput = screen.getByLabelText( 'Hours' );
await user.clear( hoursInput );
await user.type( hoursInput, '22' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T22:00:00' );
} );
it( 'should call onChange with a bounded minute if out of bounds', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const minutesInput = screen.getByLabelText( 'Minutes' );
await user.clear( minutesInput );
await user.type( minutesInput, '99' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T11:59:00' );
} );
it( 'should switch to PM correctly', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const pmButton = screen.getByText( 'PM' );
await user.click( pmButton );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T23:00:00' );
} );
it( 'should switch to AM correctly', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T23:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const amButton = screen.getByText( 'AM' );
await user.click( amButton );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T11:00:00' );
} );
it( 'should allow to set the time correctly when the PM period is selected', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
const pmButton = screen.getByText( 'PM' );
await user.click( pmButton );
const hoursInput = screen.getByLabelText( 'Hours' );
await user.clear( hoursInput );
await user.type( hoursInput, '6' );
await user.keyboard( '{Tab}' );
// When clicking on 'PM', we expect the time to be 11pm
expect( onChangeSpy ).toHaveBeenNthCalledWith(
1,
'1986-10-18T23:00:00'
);
// When changing the hours to '6', we expect the time to be 6pm
expect( onChangeSpy ).toHaveBeenNthCalledWith(
2,
'1986-10-18T18:00:00'
);
} );
it( 'should truncate at the minutes on change', async () => {
const user = userEvent.setup();
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime="1986-10-18T23:12:35"
onChange={ onChangeSpy }
is12Hour
/>
);
const minutesInput = screen.getByLabelText( 'Minutes' );
await user.clear( minutesInput );
await user.type( minutesInput, '22' );
await user.keyboard( '{Tab}' );
expect( onChangeSpy ).toHaveBeenCalledWith( '1986-10-18T23:22:00' );
} );
it( 'should reset the date when currentTime changed', () => {
const onChangeSpy = jest.fn();
const { rerender } = render(
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
rerender(
<TimePicker
currentTime="2020-07-13T18:00:00"
onChange={ onChangeSpy }
is12Hour
/>
);
expect(
( screen.getByLabelText( 'Month' ) as HTMLInputElement ).value
).toBe( '07' );
expect(
( screen.getByLabelText( 'Day' ) as HTMLInputElement ).value
).toBe( '13' );
expect(
( screen.getByLabelText( 'Year' ) as HTMLInputElement ).value
).toBe( '2020' );
expect(
( screen.getByLabelText( 'Hours' ) as HTMLInputElement ).value
).toBe( '06' );
expect(
( screen.getByLabelText( 'Minutes' ) as HTMLInputElement ).value
).toBe( '00' );
/**
* This is not ideal, but best of we can do for now until we refactor
* AM/PM into accessible elements, like radio buttons.
*/
expect( screen.getByText( 'AM' ) ).not.toHaveClass( 'is-primary' );
expect( screen.getByText( 'PM' ) ).toHaveClass( 'is-primary' );
} );
it( 'should have different layouts/orders for 12/24 hour formats', () => {
const onChangeSpy = jest.fn();
const { rerender } = render(
<form aria-label="form">
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour={ false }
/>
</form>
);
const form = screen.getByRole( 'form' ) as HTMLFormElement;
let monthInputIndex = Array.from( form.elements ).indexOf(
screen.getByLabelText( 'Month' )
);
let dayInputIndex = Array.from( form.elements ).indexOf(
screen.getByLabelText( 'Day' )
);
expect( monthInputIndex > dayInputIndex ).toBe( true );
rerender(
<form aria-label="form">
<TimePicker
currentTime="1986-10-18T11:00:00"
onChange={ onChangeSpy }
is12Hour
/>
</form>
);
monthInputIndex = Array.from( form.elements ).indexOf(
screen.getByLabelText( 'Month' )
);
dayInputIndex = Array.from( form.elements ).indexOf(
screen.getByLabelText( 'Day' )
);
expect( monthInputIndex < dayInputIndex ).toBe( true );
} );
it( 'Should set a time when passed a null currentTime', () => {
const onChangeSpy = jest.fn();
render(
<TimePicker
currentTime={ null }
onChange={ onChangeSpy }
is12Hour
/>
);
const monthInput = (
screen.getByLabelText( 'Month' ) as HTMLInputElement
).value;
const dayInput = ( screen.getByLabelText( 'Day' ) as HTMLInputElement )
.value;
const yearInput = (
screen.getByLabelText( 'Year' ) as HTMLInputElement
).value;
const hoursInput = (
screen.getByLabelText( 'Hours' ) as HTMLInputElement
).value;
const minutesInput = (
screen.getByLabelText( 'Minutes' ) as HTMLInputElement
).value;
expect( Number.isNaN( parseInt( monthInput, 10 ) ) ).toBe( false );
expect( Number.isNaN( parseInt( dayInput, 10 ) ) ).toBe( false );
expect( Number.isNaN( parseInt( yearInput, 10 ) ) ).toBe( false );
expect( Number.isNaN( parseInt( hoursInput, 10 ) ) ).toBe( false );
expect( Number.isNaN( parseInt( minutesInput, 10 ) ) ).toBe( false );
} );
} );

View File

@@ -0,0 +1,61 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { getSettings as getDateSettings } from '@wordpress/date';
/**
* Internal dependencies
*/
import Tooltip from '../../tooltip';
import { TimeZone as StyledComponent } from './styles';
/**
* Displays timezone information when user timezone is different from site
* timezone.
*/
const TimeZone = () => {
const { timezone } = getDateSettings();
// Convert timezone offset to hours.
const userTimezoneOffset = -1 * ( new Date().getTimezoneOffset() / 60 );
// System timezone and user timezone match, nothing needed.
// Compare as numbers because it comes over as string.
if ( Number( timezone.offset ) === userTimezoneOffset ) {
return null;
}
const offsetSymbol = Number( timezone.offset ) >= 0 ? '+' : '';
const zoneAbbr =
'' !== timezone.abbr && isNaN( Number( timezone.abbr ) )
? timezone.abbr
: `UTC${ offsetSymbol }${ timezone.offset }`;
// Replace underscore with space in strings like `America/Costa_Rica`.
const prettyTimezoneString = timezone.string.replace( '_', ' ' );
const timezoneDetail =
'UTC' === timezone.string
? __( 'Coordinated Universal Time' )
: `(${ zoneAbbr }) ${ prettyTimezoneString }`;
// When the prettyTimezoneString is empty, there is no additional timezone
// detail information to show in a Tooltip.
const hasNoAdditionalTimezoneDetail =
prettyTimezoneString.trim().length === 0;
return hasNoAdditionalTimezoneDetail ? (
<StyledComponent className="components-datetime__timezone">
{ zoneAbbr }
</StyledComponent>
) : (
<Tooltip placement="top" text={ timezoneDetail }>
<StyledComponent className="components-datetime__timezone">
{ zoneAbbr }
</StyledComponent>
</Tooltip>
);
};
export default TimeZone;