Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions skills/carbon-react/components/time.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,42 @@ description: Carbon Time component props and usage examples.
- Props interface: `TimeProps`

## Props
| Name | Type | Required | Literals | Description | Default |
| --- | --- | --- | --- | --- | --- |
| onChange | (ev: TimeInputEvent) => void | Yes | | Callback to handle change events in input elements | |
| value | TimeValue | Yes | | The value of the input elements | |
| disabled | boolean \| undefined | No | | If true, the component will be disabled | |
| fieldLabelsAlign | "left" \| "right" \| undefined | No | | Field labels alignment | |
| hoursInputProps | TimeInputProps \| undefined | No | | Set custom `data-` and `id` attributes on the input element. Set the `label` and `aria-label` values for the associated Label element. Set the `error` and `warning` states for the input | |
| inputHint | string \| undefined | No | | Additional hint text rendered above the input elements | |
| label | string \| undefined | No | | Label text for the component | |
| labelAlign | "left" \| "right" \| undefined | No | | Label alignment | |
| m | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on top, left, bottom and right | |
| margin | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on top, left, bottom and right | |
| marginBottom | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on bottom | |
| marginLeft | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on left | |
| marginRight | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on right | |
| marginTop | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on top | |
| marginX | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on left and right | |
| marginY | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on top and bottom | |
| mb | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on bottom | |
| minutesInputProps | TimeInputProps \| undefined | No | | Set custom `data-` and `id` attributes on the input element. Set the `label` and `aria-label` values for the associated Label element. Set the `error` and `warning` states for the input | |
| ml | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on left | |
| mr | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on right | |
| mt | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on top | |
| mx | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on left and right | |
| my | ResponsiveValue<TVal, ThemeType> \| undefined | No | | Margin on top and bottom | |
| name | string \| undefined | No | | Set a name value on the component | |
| onBlur | ((ev?: React.FocusEvent<HTMLInputElement>, value?: TimeValue) => void) \| undefined | No | | Callback called when focus is lost on input elements | |
| readOnly | boolean \| undefined | No | | If true, the component will be read-only | |
| required | boolean \| undefined | No | | Flag to configure component as mandatory | |
| size | Sizes \| undefined | No | | Sets the size of the inputs | |
| toggleProps | ToggleDataProps \| undefined | No | | Set custom data- attributes on the toggle elements | |
| validationMessagePositionTop | boolean \| undefined | No | | Render the ValidationMessage above the Time inputs | |
| data-element | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
| data-role | string \| undefined | No | | Identifier used for testing purposes, applied to the root element of the component. | |
| Name | Type | Required | Literals | Deprecated | Deprecation reason | Description | Default |
| --- | --- | --- | --- | --- | --- | --- | --- |
| onChange | (ev: TimeInputEvent) => void | Yes | | | | Callback to handle change events in input elements | |
| value | TimeValue | Yes | | | | The value of the input elements | |
| disabled | boolean \| undefined | No | | | | If true, the component will be disabled | |
| hoursInputProps | TimeInputProps \| undefined | No | | | | Set custom `data-` and `id` attributes on the input element. Set the `label` and `aria-label` values for the associated Label element. Set the `error` and `warning` states for the input | |
| legend | string \| undefined | No | | | | Legend text for the component | |
| legendHint | string \| undefined | No | | | | Additional hint text rendered below the legend | |
| m | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on top, left, bottom and right | |
| margin | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on top, left, bottom and right | |
| marginBottom | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on bottom | |
| marginLeft | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on left | |
| marginRight | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on right | |
| marginTop | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on top | |
| marginX | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on left and right | |
| marginY | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on top and bottom | |
| mb | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on bottom | |
| minutesInputProps | TimeInputProps \| undefined | No | | | | Set custom `data-` and `id` attributes on the input element. Set the `label` and `aria-label` values for the associated Label element. Set the `error` and `warning` states for the input | |
| ml | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on left | |
| mr | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on right | |
| mt | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on top | |
| mx | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on left and right | |
| my | ResponsiveValue<TVal, ThemeType> \| undefined | No | | | | Margin on top and bottom | |
| name | string \| undefined | No | | | | Set a name value on the component | |
| onBlur | ((ev?: React.FocusEvent<HTMLInputElement>, value?: TimeValue) => void) \| undefined | No | | | | Callback called when focus is lost on input elements | |
| readOnly | boolean \| undefined | No | | | | If true, the component will be read-only | |
| required | boolean \| undefined | No | | | | Flag to configure component as mandatory | |
| size | Sizes \| undefined | No | | | | Sets the size of the inputs | |
| toggleProps | ToggleDataProps \| undefined | No | | | | Set custom data- attributes on the toggle elements | |
| validationMessagePositionTop | boolean \| undefined | No | | | | Render the ValidationMessage above the Time inputs | |
| data-element | string \| undefined | No | | | | Identifier used for testing purposes, applied to the root element of the component. | |
| data-role | string \| undefined | No | | | | Identifier used for testing purposes, applied to the root element of the component. | |
| fieldLabelsAlign | "left" \| "right" \| undefined | No | | Yes | Alignment is no longer supported and this prop will be removed | | |
| inputHint | string \| undefined | No | | Yes | Use `legendHint` instead. Additional hint text rendered below the legend | | |
| label | string \| undefined | No | | Yes | Use `legend` instead. Label text for the component | | |
| labelAlign | "left" \| "right" \| undefined | No | | Yes | Alignment is no longer supported and this prop will be removed | | |

## Examples
### Default
Expand Down
10 changes: 10 additions & 0 deletions src/__internal__/fieldset/fieldset.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ test("renders fieldset with provided `legend`", () => {
expect(fieldset).toBeVisible();
});

test("renders with provided `width`", () => {
render(
<Fieldset width="288px">
<input title="Test" placeholder="Placeholder" />
</Fieldset>,
);

expect(screen.getByRole("group")).toHaveStyle({ width: "288px" });
});

test("sets child inputs as required when `isRequired` is true", () => {
render(
<Fieldset isRequired>
Expand Down
89 changes: 51 additions & 38 deletions src/components/time/time.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import useLocale from "../../hooks/__internal__/useLocale";
import tagComponent from "../../__internal__/utils/helpers/tags/tags";
import { filterStyledSystemMarginProps } from "../../style/utils";

import Fieldset from "../../__internal__/fieldset";
import Fieldset from "../../__internal__/fieldset/__next__/fieldset.component";
import Box from "../box";
import Number from "../number";
import Typography from "../typography";
import StyledLabel from "./time.style";
import Textbox from "../textbox";
import Label from "../../__internal__/label";
import StyledColon from "./time.style";
import { TimeToggle, ToggleDataProps } from "./__internal__/time-toggle";
import FieldsetValidationContext from "../../__internal__/fieldset-validation-context";

Expand All @@ -41,24 +41,28 @@ export interface TimeInputEvent {
}

interface TimeInputProps extends TagProps, Omit<ValidationProps, "info"> {
/** Set an id value on the input */
/** Set an id value on the input element */
id?: string;
/** Override the default label text */
/** Override the default label text on the input */
label?: string;
/** Override the default aria-label text */
/** Override the default aria-label text on the input */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: if you're adding this to the prop description you should also add it to any other props within TimeInputProps for consistency sake

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

"aria-label"?: string;
}

export interface TimeProps extends TagProps, MarginProps {
/** Label text for the component */
/** Legend text for the component */
legend?: string;
/** Additional hint text rendered below the legend */
legendHint?: string;
/** @deprecated Use `legend` instead. Label text for the component */
label?: string;
/** Label alignment */
/** @deprecated Alignment is no longer supported and this prop will be removed */
labelAlign?: "left" | "right";
/** Field labels alignment */
/** @deprecated Alignment is no longer supported and this prop will be removed */
fieldLabelsAlign?: "left" | "right";
/** Sets the size of the inputs */
size?: Sizes;
/** Additional hint text rendered above the input elements */
/** @deprecated Use `legendHint` instead. Additional hint text rendered below the legend */
inputHint?: string;
/**
* Set custom `data-` and `id` attributes on the input element.
Expand Down Expand Up @@ -108,9 +112,9 @@ const SIZES = {
const Time = React.forwardRef<TimeHandle, TimeProps>(
(
{
legend,
legendHint,
label,
labelAlign,
fieldLabelsAlign,
size = "medium",
inputHint,
hoursInputProps = {},
Expand All @@ -131,14 +135,14 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
const {
id: hoursInputId,
label: hoursLabel,
"aria-label": hoursAriaLabel,
"aria-label": hoursInputAriaLabel,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: hoursInputAriaLabel is definitely a better name here, makes it much clearer where the prop is being used

error: hoursError,
warning: hoursWarning,
} = hoursInputProps;
const {
id: minutesInputId,
label: minutesLabel,
"aria-label": minutesAriaLabel,
"aria-label": minutesInputAriaLabel,
error: minutesError,
warning: minutesWarning,
} = minutesInputProps;
Expand Down Expand Up @@ -168,9 +172,10 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
const [period, setPeriod] = useState(toggleValue);
const hrsLabel = hoursLabel || locale.time.hoursLabelText();
const minsLabel = minutesLabel || locale.time.minutesLabelText();
const hrsAriaLabel = hoursAriaLabel || locale.time.hoursAriaLabelText();
const hrsAriaLabel =
hoursInputAriaLabel || locale.time.hoursAriaLabelText();
const minsAriaLabel =
minutesAriaLabel || locale.time.minutesAriaLabelText();
minutesInputAriaLabel || locale.time.minutesAriaLabelText();
const hoursRef = useRef<HTMLInputElement>(null);
const minsRef = useRef<HTMLInputElement>(null);

Expand Down Expand Up @@ -202,11 +207,11 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
const computedValidations = (
hrs?: string | boolean,
mins?: string | boolean,
) => {
): string | undefined => {
const hoursIsString = typeof hrs === "string";
const minutesIsString = typeof mins === "string";
if (!hoursIsString && !minutesIsString) {
return hrs || mins;
return undefined;
}

if (hoursIsString && !minutesIsString) {
Expand Down Expand Up @@ -240,8 +245,9 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
ev: React.ChangeEvent<HTMLInputElement>,
inputName: "hrs" | "mins",
) => {
const hours = inputName === "hrs" ? ev.target.value : inputValues[0];
const minutes = inputName === "mins" ? ev.target.value : inputValues[1];
const rawValue = ev.target.value.replace(/[^0-9]/g, "");
Comment thread
tomdavies73 marked this conversation as resolved.
const hours = inputName === "hrs" ? rawValue : inputValues[0];
const minutes = inputName === "mins" ? rawValue : inputValues[1];
setInputValues([hours, minutes]);

const formattedHours = hours.length ? hours.padStart(2, "0") : hours;
Expand Down Expand Up @@ -304,12 +310,9 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(

return (
<Fieldset
applyNewValidation
id={internalId.current}
legend={label}
inputHint={inputHint}
width="min-content"
legendAlign={labelAlign}
legend={legend || label}
legendHint={legendHint || inputHint}
isRequired={required}
isDisabled={disabled}
name={name}
Expand All @@ -325,15 +328,15 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
>
<Box display="flex" className="time">
<div>
<StyledLabel
aria-label={hrsAriaLabel}
<Label
htmlFor={internalHrsId.current}
size={size}
disabled={disabled}
align={fieldLabelsAlign}
readOnly={readOnly}
>
{hrsLabel}
</StyledLabel>
<Number
</Label>
<Textbox
{...hoursInputProps}
label={undefined}
data-component="hours"
Expand All @@ -347,8 +350,11 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
warning={!!hoursWarning}
disabled={disabled}
readOnly={readOnly}
required={required}
maxWidth={SIZES[size]}
my={0} // prevents any form spacing being applied
inputMode="numeric"
aria-label={hrsAriaLabel}
/>
</div>
<Box
Expand All @@ -359,20 +365,24 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
aria-hidden="true"
>
<span>&nbsp;</span>
<Typography isDisabled={disabled} variant="span" mb="-4px">
<StyledColon
$size={size}
$isDisabled={disabled}
$isReadOnly={readOnly}
>
:
</Typography>
</StyledColon>
</Box>
Comment thread
nuria1110 marked this conversation as resolved.
<div>
<StyledLabel
aria-label={minsAriaLabel}
<Label
htmlFor={internalMinsId.current}
size={size}
disabled={disabled}
align={fieldLabelsAlign}
readOnly={readOnly}
>
{minsLabel}
</StyledLabel>
<Number
</Label>
<Textbox
{...minutesInputProps}
label={undefined}
data-component="minutes"
Expand All @@ -386,8 +396,11 @@ const Time = React.forwardRef<TimeHandle, TimeProps>(
warning={!!minutesWarning}
disabled={disabled}
readOnly={readOnly}
required={required}
maxWidth={SIZES[size]}
my={0} // prevents any form spacing being applied
inputMode="numeric"
aria-label={minsAriaLabel}
/>
</div>
{showToggle && (
Expand Down
3 changes: 1 addition & 2 deletions src/components/time/time.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ test.describe("Time component", () => {
await checkAccessibility(page);
});

// disabled styling for label, input hint etc fail colour contrast
test("should pass when toggle control rendered and disabled prop set", async ({
mount,
page,
Expand All @@ -129,7 +128,7 @@ test.describe("Time component", () => {
/>,
);

await checkAccessibility(page, undefined, "color-contrast");
await checkAccessibility(page);
});

test("should pass when toggle control rendered and readOnly prop set", async ({
Expand Down
37 changes: 30 additions & 7 deletions src/components/time/time.style.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import styled from "styled-components";
import Label from "../../__internal__/legacy-label";
import styled, { css } from "styled-components";

const StyledLabel = styled(Label)`
label {
font-weight: var(--fontWeights400);
}
const colonFontBySize = {
small: "var(--global-font-static-comp-regular-s)",
medium: "var(--global-font-static-comp-regular-m)",
large: "var(--global-font-static-comp-regular-l)",
};

type StyledColonProps = {
$size: "small" | "medium" | "large";
$isDisabled?: boolean;
$isReadOnly?: boolean;
};

const StyledColon = styled.span<StyledColonProps>`
${({ $size, $isDisabled, $isReadOnly }) => css`
font: ${colonFontBySize[$size]};
color: var(--input-typical-txt-default);
text-align: center;

${$isReadOnly &&
css`
color: var(--input-typical-txt-read-only);
`}

${$isDisabled &&
css`
color: var(--input-typical-txt-disabled);
`}
`}
`;

export default StyledLabel;
export default StyledColon;
Loading
Loading