diff --git a/packages/input-box/src/InputBoxContext/InputBoxContext.spec.tsx b/packages/input-box/src/InputBoxContext/InputBoxContext.spec.tsx new file mode 100644 index 0000000000..6f2b22a0db --- /dev/null +++ b/packages/input-box/src/InputBoxContext/InputBoxContext.spec.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +import { isReact17, renderHook } from '@leafygreen-ui/testing-lib'; +import { Size } from '@leafygreen-ui/tokens'; + +import { + charsPerSegmentMock, + SegmentObjMock, + segmentRefsMock, + segmentsMock, +} from '../testutils/testutils.mocks'; + +import { InputBoxProvider, useInputBoxContext } from './InputBoxContext'; + +describe('InputBoxContext', () => { + const mockOnChange = jest.fn(); + const mockOnBlur = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('throws error when used outside of InputBoxProvider', () => { + /** + * The version of `renderHook` imported from "@testing-library/react-hooks", (used in React 17) + * has an error boundary, and doesn't throw errors as expected: + * https://github.com/testing-library/react-hooks-testing-library/blob/main/src/index.ts#L5 + * */ + if (isReact17()) { + const { result } = renderHook(() => useInputBoxContext()); + expect(result.error.message).toEqual( + 'useInputBoxContext must be used within a InputBoxProvider', + ); + } else { + expect(() => + renderHook(() => useInputBoxContext()), + ).toThrow('useInputBoxContext must be used within a InputBoxProvider'); + } + }); + + test('provides context values that match the props passed to the provider', () => { + const { result } = renderHook(() => useInputBoxContext(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + const { + charsPerSegment, + segmentEnum, + onChange, + onBlur, + segmentRefs, + segments, + size, + disabled, + } = result.current; + + expect(charsPerSegment).toBe(charsPerSegmentMock); + expect(segmentEnum).toBe(SegmentObjMock); + expect(onChange).toBe(mockOnChange); + expect(onBlur).toBe(mockOnBlur); + expect(segmentRefs).toBe(segmentRefsMock); + expect(segments).toBe(segmentsMock); + expect(size).toBe(Size.Default); + expect(disabled).toBe(false); + }); +}); diff --git a/packages/input-box/src/InputBoxContext/InputBoxContext.tsx b/packages/input-box/src/InputBoxContext/InputBoxContext.tsx new file mode 100644 index 0000000000..db487b6a7a --- /dev/null +++ b/packages/input-box/src/InputBoxContext/InputBoxContext.tsx @@ -0,0 +1,78 @@ +import React, { + createContext, + PropsWithChildren, + useContext, + useMemo, +} from 'react'; + +import { + InputBoxContextType, + InputBoxProviderProps, +} from './InputBoxContext.types'; + +// The Context constant is defined with the default/fixed type, which is string. This is the loose type because we don't know the type of the string yet. +export const InputBoxContext = createContext(null); + +// Provider is generic over T, the string union +export const InputBoxProvider = ({ + charsPerSegment, + children, + disabled, + labelledBy, + onChange, + onBlur, + segments, + segmentEnum, + segmentRefs, + size, +}: PropsWithChildren>) => { + const value = useMemo( + () => ({ + charsPerSegment, + children, + disabled, + labelledBy, + onChange, + onBlur, + segments, + segmentEnum, + segmentRefs, + size, + }), + [ + charsPerSegment, + children, + disabled, + labelledBy, + onChange, + onBlur, + segments, + segmentEnum, + segmentRefs, + size, + ], + ); + + // The provider passes a strict type of T but the context is defined as a loose type of string so TS sees a potential type mismatch. This assertion says that we know that the types do not overlap but we guarantee that the strict provider value satisfies the fixed context requirement. + return ( + + {children} + + ); +}; + +// The hook is generic over T, the string union +export const useInputBoxContext = () => { + // Assert the context type to the specific generic T + const context = useContext( + InputBoxContext, + ) as InputBoxContextType | null; + + if (!context) { + throw new Error( + 'useInputBoxContext must be used within a InputBoxProvider', + ); + } + + return context; +}; diff --git a/packages/input-box/src/InputBoxContext/InputBoxContext.types.ts b/packages/input-box/src/InputBoxContext/InputBoxContext.types.ts new file mode 100644 index 0000000000..0834e83062 --- /dev/null +++ b/packages/input-box/src/InputBoxContext/InputBoxContext.types.ts @@ -0,0 +1,20 @@ +import { + InputSegmentChangeEventHandler, + SharedInputBoxTypes, +} from '../shared.types'; + +export interface InputBoxContextType + extends SharedInputBoxTypes { + /** + * The handler for the onChange event that will be read in the InputSegment component + */ + onChange: InputSegmentChangeEventHandler; + + /** + * The handler for the onBlur event that will be read by the InputSegment component + */ + onBlur: (event: React.FocusEvent) => void; +} + +export interface InputBoxProviderProps + extends InputBoxContextType {} diff --git a/packages/input-box/src/InputBoxContext/index.ts b/packages/input-box/src/InputBoxContext/index.ts new file mode 100644 index 0000000000..226a86c6bb --- /dev/null +++ b/packages/input-box/src/InputBoxContext/index.ts @@ -0,0 +1,9 @@ +export { + InputBoxContext, + InputBoxProvider, + useInputBoxContext, +} from './InputBoxContext'; +export type { + InputBoxContextType, + InputBoxProviderProps, +} from './InputBoxContext.types'; diff --git a/packages/input-box/src/InputSegment/InputSegment.types.ts b/packages/input-box/src/InputSegment/InputSegment.types.ts new file mode 100644 index 0000000000..e39a43f7e7 --- /dev/null +++ b/packages/input-box/src/InputSegment/InputSegment.types.ts @@ -0,0 +1 @@ +// This file is a placeholder for the InputSegment types. diff --git a/packages/input-box/src/InputSegment/index.ts b/packages/input-box/src/InputSegment/index.ts new file mode 100644 index 0000000000..932ef6b10b --- /dev/null +++ b/packages/input-box/src/InputSegment/index.ts @@ -0,0 +1 @@ +// Export the InputSegment component diff --git a/packages/input-box/src/shared.types.ts b/packages/input-box/src/shared.types.ts new file mode 100644 index 0000000000..87e6e31264 --- /dev/null +++ b/packages/input-box/src/shared.types.ts @@ -0,0 +1,83 @@ +import { keyMap } from '@leafygreen-ui/lib'; +import { Size } from '@leafygreen-ui/tokens'; + +/** + * Shared Input Segment Change Event + */ +export interface InputSegmentChangeEvent< + Segment extends string, + Value extends string, +> { + segment: Segment; + value: Value; + meta?: { + key?: (typeof keyMap)[keyof typeof keyMap]; + min: number; + [key: string]: any; + }; +} + +/** + * The type for the onChange handler + */ +export type InputSegmentChangeEventHandler< + Segment extends string, + Value extends string, +> = (inputSegmentChangeEvent: InputSegmentChangeEvent) => void; + +/** + * Shared Input Box Types + */ +export interface SharedInputBoxTypes { + /** + * The number of characters per segment + * + * @example + * { day: 2, month: 2, year: 4 } + */ + charsPerSegment: Record; + + /** + * An enumerable object that maps the segment names to their values + * + * @example + * { Day: 'day', Month: 'month', Year: 'year' } + */ + segmentEnum: Record; + + /** + * An object that maps the segment names to their refs + * + * @example + * { day: ref, month: ref, year: ref } + */ + segmentRefs: Record>; + + /** + * An object containing the values of the segments + * + * @example + * { day: '1', month: '2', year: '2025' } + */ + segments: Record; + + /** + * The size of the input box + * + * @example + * Size.Default + * Size.Small + * Size.Large + */ + size: Size; + + /** + * Whether the input box is disabled + */ + disabled: boolean; + + /** + * id of the labelling element + */ + labelledBy?: string; +} diff --git a/packages/input-box/src/testutils/testutils.mocks.ts b/packages/input-box/src/testutils/testutils.mocks.ts new file mode 100644 index 0000000000..0466e233e3 --- /dev/null +++ b/packages/input-box/src/testutils/testutils.mocks.ts @@ -0,0 +1,85 @@ +import { createRef } from 'react'; + +import { css } from '@leafygreen-ui/emotion'; +import { DynamicRefGetter } from '@leafygreen-ui/hooks'; + +import { ExplicitSegmentRule } from '../utils'; + +export const SegmentObjMock = { + Month: 'month', + Day: 'day', + Year: 'year', +} as const; +export type SegmentObjMock = + (typeof SegmentObjMock)[keyof typeof SegmentObjMock]; + +export type SegmentRefsMock = Record< + SegmentObjMock, + ReturnType> +>; + +export const segmentRefsMock: SegmentRefsMock = { + month: createRef(), + day: createRef(), + year: createRef(), +}; + +export const segmentsMock: Record = { + month: '02', + day: '02', + year: '2025', +}; +export const charsPerSegmentMock: Record = { + month: 2, + day: 2, + year: 4, +}; +export const segmentRulesMock: Record = { + month: { maxChars: 2, minExplicitValue: 2 }, + day: { maxChars: 2, minExplicitValue: 4 }, + year: { maxChars: 4, minExplicitValue: 1970 }, +}; +export const defaultMinMock: Record = { + month: 1, + day: 0, + year: 1970, +}; +export const defaultMaxMock: Record = { + month: 12, + day: 31, + year: 2038, +}; + +export const defaultPlaceholderMock: Record = { + day: 'DD', + month: 'MM', + year: 'YYYY', +} as const; + +export const defaultFormatPartsMock: Array = [ + { type: 'month', value: '' }, + { type: 'literal', value: '-' }, + { type: 'day', value: '' }, + { type: 'literal', value: '-' }, + { type: 'year', value: '' }, +]; + +/** The percentage of 1ch these specific characters take up */ +export const characterWidth = { + // Standard font + D: 46 / 40, + M: 55 / 40, + Y: 50 / 40, +} as const; + +export const segmentWidthStyles: Record = { + day: css` + width: ${charsPerSegmentMock.day * characterWidth.D}ch; + `, + month: css` + width: ${charsPerSegmentMock.month * characterWidth.M}ch; + `, + year: css` + width: ${charsPerSegmentMock.year * characterWidth.Y}ch; + `, +}; diff --git a/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.spec.ts b/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.spec.ts index 535e7096a1..df950376f3 100644 --- a/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.spec.ts +++ b/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.spec.ts @@ -24,115 +24,362 @@ const isExplicitSegmentValue = createExplicitSegmentValidator({ describe('packages/input-box/utils/createExplicitSegmentValidator', () => { describe('day segment', () => { test('returns false for single digit below minExplicitValue', () => { - expect(isExplicitSegmentValue('day', '1')).toBe(false); - expect(isExplicitSegmentValue('day', '2')).toBe(false); - expect(isExplicitSegmentValue('day', '3')).toBe(false); + expect(isExplicitSegmentValue({ segment: 'day', value: '1' })).toBe( + false, + ); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '2', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '3', + }), + ).toBe(false); }); test('returns true for single digit at or above minExplicitValue', () => { - expect(isExplicitSegmentValue('day', '4')).toBe(true); - expect(isExplicitSegmentValue('day', '5')).toBe(true); - expect(isExplicitSegmentValue('day', '9')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '4', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '5', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '9', + }), + ).toBe(true); }); test('returns true for two-digit values (maxChars)', () => { - expect(isExplicitSegmentValue('day', '01')).toBe(true); - expect(isExplicitSegmentValue('day', '10')).toBe(true); - expect(isExplicitSegmentValue('day', '22')).toBe(true); - expect(isExplicitSegmentValue('day', '31')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '01', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '10', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '22', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '31', + }), + ).toBe(true); }); test('returns false for invalid values', () => { - expect(isExplicitSegmentValue('day', '0')).toBe(false); - expect(isExplicitSegmentValue('day', '')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '0', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ segment: 'day', value: '', allowZero: false }), + ).toBe(false); }); }); describe('month segment', () => { test('returns false for single digit below minExplicitValue', () => { - expect(isExplicitSegmentValue('month', '1')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '1', + }), + ).toBe(false); }); test('returns true for single digit at or above minExplicitValue', () => { - expect(isExplicitSegmentValue('month', '2')).toBe(true); - expect(isExplicitSegmentValue('month', '3')).toBe(true); - expect(isExplicitSegmentValue('month', '9')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '2', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '3', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '9', + }), + ).toBe(true); }); test('returns true for two-digit values (maxChars)', () => { - expect(isExplicitSegmentValue('month', '01')).toBe(true); - expect(isExplicitSegmentValue('month', '12')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '01', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '12', + }), + ).toBe(true); }); test('returns false for invalid values', () => { - expect(isExplicitSegmentValue('month', '0')).toBe(false); - expect(isExplicitSegmentValue('month', '')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '0', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'month', + value: '', + }), + ).toBe(false); }); }); describe('year segment', () => { test('returns false for values shorter than maxChars', () => { - expect(isExplicitSegmentValue('year', '1')).toBe(false); - expect(isExplicitSegmentValue('year', '20')).toBe(false); - expect(isExplicitSegmentValue('year', '200')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '1', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '20', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '200', + }), + ).toBe(false); }); test('returns true for four-digit values (maxChars)', () => { - expect(isExplicitSegmentValue('year', '1970')).toBe(true); - expect(isExplicitSegmentValue('year', '2000')).toBe(true); - expect(isExplicitSegmentValue('year', '2023')).toBe(true); - expect(isExplicitSegmentValue('year', '0001')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '1970', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '2000', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '2023', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '0001', + }), + ).toBe(true); }); test('returns false for invalid values', () => { - expect(isExplicitSegmentValue('year', '0')).toBe(false); - expect(isExplicitSegmentValue('year', '')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '0', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'year', + value: '', + }), + ).toBe(false); }); }); describe('hour segment', () => { test('returns false for single digit below minExplicitValue', () => { - expect(isExplicitSegmentValue('hour', '1')).toBe(false); - expect(isExplicitSegmentValue('hour', '0')).toBe(false); - expect(isExplicitSegmentValue('hour', '2')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '1', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '0', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '2', + }), + ).toBe(false); }); test('returns true for single digit at or above minExplicitValue', () => { - expect(isExplicitSegmentValue('hour', '3')).toBe(true); - expect(isExplicitSegmentValue('hour', '9')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '3', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '9', + }), + ).toBe(true); }); test('returns true for two-digit values at or above minExplicitValue', () => { - expect(isExplicitSegmentValue('hour', '12')).toBe(true); - expect(isExplicitSegmentValue('hour', '23')).toBe(true); - expect(isExplicitSegmentValue('hour', '05')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '12', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '23', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'hour', + value: '05', + }), + ).toBe(true); }); }); describe('minute segment', () => { test('returns false for single digit below minExplicitValue', () => { - expect(isExplicitSegmentValue('minute', '0')).toBe(false); - expect(isExplicitSegmentValue('minute', '1')).toBe(false); - expect(isExplicitSegmentValue('minute', '5')).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '0', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '1', + }), + ).toBe(false); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '5', + }), + ).toBe(false); }); test('returns true for single digit at or above minExplicitValue', () => { - expect(isExplicitSegmentValue('minute', '6')).toBe(true); - expect(isExplicitSegmentValue('minute', '7')).toBe(true); - expect(isExplicitSegmentValue('minute', '9')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '6', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '7', + }), + ).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '9', + }), + ).toBe(true); }); test('returns true for two-digit values at or above minExplicitValue', () => { - expect(isExplicitSegmentValue('minute', '59')).toBe(true); + expect( + isExplicitSegmentValue({ + segment: 'minute', + value: '59', + }), + ).toBe(true); + }); + }); + + describe('allowZero', () => { + test('returns false when allowZero is false', () => { + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '00', + allowZero: false, + }), + ).toBe(false); + }); + + test('returns true when allowZero is true', () => { + expect( + isExplicitSegmentValue({ + segment: 'day', + value: '00', + allowZero: true, + }), + ).toBe(true); }); }); describe('invalid segment names', () => { test('returns false for unknown segment names', () => { - // @ts-expect-error Testing invalid segment - expect(isExplicitSegmentValue('invalid', '10')).toBe(false); - // @ts-expect-error Testing invalid segment - expect(isExplicitSegmentValue('millisecond', '12')).toBe(false); + expect( + isExplicitSegmentValue({ + // @ts-expect-error Testing invalid segment + segment: 'invalid', + value: '10', + }), + ).toBe(false); + + expect( + isExplicitSegmentValue({ + // @ts-expect-error Testing invalid segment + segment: 'millisecond', + value: '12', + }), + ).toBe(false); }); }); }); diff --git a/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.ts b/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.ts index 3c0ed0b910..cd3396a34c 100644 --- a/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.ts +++ b/packages/input-box/src/utils/createExplicitSegmentValidator/createExplicitSegmentValidator.ts @@ -25,7 +25,11 @@ export interface ExplicitSegmentRule { * * @param segmentEnum - The segment enum/object containing the segment names and their corresponding values to validate against * @param rules - Rules for each segment type - * @returns A function that checks if a segment value is explicit + * @returns A function that checks if a segment value is explicit and accepts the segment, value, and allowZero parameters + * + * @param segment - The segment to validate + * @param value - The value to validate + * @param allowZero - Whether to allow zero values * * @example * const segmentObj = { @@ -39,7 +43,6 @@ export interface ExplicitSegmentRule { * day: { maxChars: 2, minExplicitValue: 4 }, * month: { maxChars: 2, minExplicitValue: 2 }, * year: { maxChars: 4 }, - * hour: { maxChars: 2, minExplicitValue: 3 }, * minute: { maxChars: 2, minExplicitValue: 6 }, * }; * @@ -52,17 +55,14 @@ export interface ExplicitSegmentRule { * rules, * }); * - * isExplicitSegmentValue('day', '1'); // false (Ambiguous - below min value and max length) - * isExplicitSegmentValue('day', '01'); // true (Explicit - meets max length) - * isExplicitSegmentValue('day', '4'); // true (Explicit - meets min value) - * isExplicitSegmentValue('year', '2000'); // true (Explicit - meets max length) - * isExplicitSegmentValue('year', '1'); // false (Ambiguous - below max length) - * isExplicitSegmentValue('hour', '05'); // true (Explicit - meets min value) - * isExplicitSegmentValue('hour', '23'); // true (Explicit - meets max length) - * isExplicitSegmentValue('hour', '2'); // false (Ambiguous - below min value) - * isExplicitSegmentValue('minute', '07'); // true (Explicit - meets min value) - * isExplicitSegmentValue('minute', '59'); // true (Explicit - meets max length) - * isExplicitSegmentValue('minute', '5'); // false (Ambiguous - below min value) + * isExplicitSegmentValue({ segment: 'day', value: '1', allowZero: false }); // false (Ambiguous - below min value and max length) + * isExplicitSegmentValue({ segment: 'day', value: '01', allowZero: false }); // true (Explicit - meets max length) + * isExplicitSegmentValue({ segment: 'day', value: '4', allowZero: false }); // true (Explicit - meets min value) + * isExplicitSegmentValue({ segment: 'year', value: '2000', allowZero: false }); // true (Explicit - meets max length) + * isExplicitSegmentValue({ segment: 'year', value: '1', allowZero: false }); // false (Ambiguous - below max length) + * isExplicitSegmentValue({ segment: 'minute', value: '07', allowZero: false }); // true (Explicit - meets min value) + * isExplicitSegmentValue({ segment: 'minute', value: '59', allowZero: false }); // true (Explicit - meets max length) + * isExplicitSegmentValue({ segment: 'minute', value: '5', allowZero: false }); // false (Ambiguous - below min value) */ export function createExplicitSegmentValidator< SegmentEnum extends Record, @@ -73,11 +73,23 @@ export function createExplicitSegmentValidator< segmentEnum: SegmentEnum; rules: Record; }) { - return (segment: SegmentEnum[keyof SegmentEnum], value: string): boolean => { + return ({ + segment, + value, + allowZero = false, + }: { + segment: SegmentEnum[keyof SegmentEnum]; + value: string; + allowZero?: boolean; + }): boolean => { if ( - !(isValidSegmentValue(value) && isValidSegmentName(segmentEnum, segment)) - ) + !( + isValidSegmentValue(value, allowZero) && + isValidSegmentName(segmentEnum, segment) + ) + ) { return false; + } const rule = rules[segment]; if (!rule) return false;